This commit is contained in:
Home Assistant Version Control
2026-07-24 15:19:02 +00:00
parent 8abf4efaec
commit 2d21a40fd6
48 changed files with 4181 additions and 232 deletions
+212 -30
View File
@@ -61,12 +61,24 @@ from .const import (
CONF_SYNOLOGY_SPACE,
CONF_SYNOLOGY_ALBUM_ID,
CONF_SYNOLOGY_IMAGE_SIZE,
CONF_SYNOLOGY_PASSPHRASE,
CONF_SYNOLOGY_FAVORITE,
CONF_SYNOLOGY_SELECTION,
DEFAULT_SYNOLOGY_IMAGE_SIZE,
SYNOLOGY_SPACE_PERSONAL,
SYNOLOGY_SPACE_SHARED,
SYNOLOGY_IMAGE_SMALL,
SYNOLOGY_IMAGE_MEDIUM,
SYNOLOGY_IMAGE_LARGE,
CONF_NEXTCLOUD_URL,
CONF_NEXTCLOUD_USERNAME,
CONF_NEXTCLOUD_PASSWORD,
CONF_NEXTCLOUD_FOLDER,
CONF_NEXTCLOUD_RECURSIVE,
CONF_NEXTCLOUD_IMAGE_SIZE,
DEFAULT_NEXTCLOUD_IMAGE_SIZE,
NEXTCLOUD_IMAGE_PREVIEW,
NEXTCLOUD_IMAGE_ORIGINAL,
DEFAULT_REVERSE_GEOCODE,
PROVIDER_GOOGLE_SHARED,
PROVIDER_LOCAL_FOLDER,
@@ -75,6 +87,7 @@ from .const import (
PROVIDER_PHOTOPRISM,
PROVIDER_ICLOUD,
PROVIDER_SYNOLOGY,
PROVIDER_NEXTCLOUD,
DEFAULT_RECURSIVE,
)
@@ -129,6 +142,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._syn_device_id: str | None = None
self._syn_space: str = SYNOLOGY_SPACE_PERSONAL
self._syn_albums: dict[str, str] = {}
# option key -> {"album_id": id|None, "passphrase": str|None}
self._syn_album_meta: dict[str, dict[str, Any]] = {}
# id(str) -> name maps for the composite category multi-selects.
self._syn_people: dict[str, str] = {}
self._syn_places: dict[str, str] = {}
self._syn_tags: dict[str, str] = {}
self._syn_subjects: dict[str, str] = {}
@staticmethod
@callback
@@ -147,7 +167,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
``__init__`` raises (the symptom is a 500 when the user clicks
Configure).
"""
if config_entry.data.get(CONF_PROVIDER) == PROVIDER_LOCAL_FOLDER:
if config_entry.data.get(CONF_PROVIDER) in (
PROVIDER_LOCAL_FOLDER,
PROVIDER_NEXTCLOUD,
):
return LocalFolderOptionsFlow()
return _NoOptionsFlow()
@@ -166,6 +189,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_icloud()
if self._provider == PROVIDER_SYNOLOGY:
return await self.async_step_synology()
if self._provider == PROVIDER_NEXTCLOUD:
return await self.async_step_nextcloud()
return await self.async_step_google_shared()
schema = vol.Schema(
@@ -177,6 +202,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)",
PROVIDER_ICLOUD: "iCloud Shared Album",
PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)",
PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder, full metadata)",
PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)",
})
}
@@ -699,9 +725,23 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
try:
await client.async_login(otp_code=otp or None)
albums = await client.async_list_albums()
# Albums and category browsing live only in the Personal space
# (there is no Shared Space album/category API). For the Shared
# Space, validate access up front so a permission problem
# surfaces here, not later.
if space == SYNOLOGY_SPACE_SHARED:
albums = people = places = tags = subjects = []
await client.async_collect_assets(None)
else:
albums = await client.async_list_albums()
people = await client.async_list_people()
places = await client.async_list_places()
tags = await client.async_list_tags()
subjects = await client.async_list_subjects()
except syn_api.SynologyOtpRequired:
errors["otp_code"] = "synology_otp_required"
except syn_api.SynologyPermissionError:
errors["base"] = "synology_shared_unavailable"
except Exception: # noqa: BLE001 - any failure means bad URL/creds
errors["base"] = "synology_cannot_connect"
else:
@@ -712,10 +752,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
# A trusted-device token is captured only on the OTP login;
# store it so future logins skip the 2FA prompt.
self._syn_device_id = client.captured_device_id
self._syn_albums = {
str(a["id"]): (a.get("name") or str(a["id"]))
for a in albums
if a.get("id") is not None
# Key each album by a synthetic value so an own album and a
# shared-with-me album that happen to share a numeric id don't
# collide. Track album_id + passphrase per option.
self._syn_albums = {}
self._syn_album_meta = {}
for a in albums:
if a.get("id") is None:
continue
shared = bool(a.get("shared"))
key = f"{'shared' if shared else 'own'}:{a['id']}"
label = a.get("name") or str(a["id"])
self._syn_albums[key] = f"{label} (shared)" if shared else label
self._syn_album_meta[key] = {
"album_id": None if shared else a["id"],
"passphrase": a.get("passphrase") if shared else None,
}
self._syn_people = {
str(p["id"]): p["name"] for p in people if p.get("id") is not None
}
self._syn_places = {
str(p["id"]): p["name"] for p in places if p.get("id") is not None
}
self._syn_tags = {
str(t["id"]): t["name"] for t in tags if t.get("id") is not None
}
self._syn_subjects = {
str(s["id"]): s["name"] for s in subjects if s.get("id") is not None
}
await client.async_logout()
return await self.async_step_synology_select()
@@ -745,7 +808,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_synology_select(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Pick a Synology album (or the whole space) and image size."""
"""Build a composite Synology selection and finish the entry.
Like the Immich/PhotoPrism providers: tick any mix of favorites,
albums, people, places, tags and subjects. Synology has no OR across
categories, so each member is queried on its own and merged. An empty
selection means the whole space.
"""
errors: dict[str, str] = {}
if user_input is not None:
@@ -753,13 +822,42 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
size = user_input.get(
CONF_SYNOLOGY_IMAGE_SIZE, DEFAULT_SYNOLOGY_IMAGE_SIZE
)
album_id = user_input.get("album")
if album_id in (None, "", "__all__") or album_id not in self._syn_albums:
album_id = ""
favorites = bool(user_input.get("favorites"))
album_ids: list[Any] = []
passphrases: list[str] = []
for key in user_input.get("albums", []) or []:
meta = self._syn_album_meta.get(key)
if not meta:
continue
if meta.get("passphrase"):
passphrases.append(meta["passphrase"])
elif meta.get("album_id") is not None:
album_ids.append(meta["album_id"])
def _ids(field: str, valid: dict[str, str]) -> list[int]:
out: list[int] = []
for v in user_input.get(field, []) or []:
if v in valid:
try:
out.append(int(v))
except (TypeError, ValueError):
pass
return out
selection = {
"favorites": favorites,
"album_ids": album_ids,
"passphrases": passphrases,
"person_ids": _ids("people", self._syn_people),
"geocoding_ids": _ids("places", self._syn_places),
"tag_ids": _ids("tags", self._syn_tags),
"concept_ids": _ids("subjects", self._syn_subjects),
}
sel_id = json.dumps(selection, sort_keys=True)
unique = (
f"{DOMAIN}:{PROVIDER_SYNOLOGY}:{self._syn_url}:"
f"{self._syn_space}:{album_id or 'all'}"
f"{self._syn_space}:{sel_id}"
)
await self.async_set_unique_id(unique)
self._abort_if_unique_id_configured()
@@ -769,44 +867,128 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_SYNOLOGY_USERNAME: self._syn_username,
CONF_SYNOLOGY_PASSWORD: self._syn_password,
CONF_SYNOLOGY_SPACE: self._syn_space,
CONF_SYNOLOGY_SELECTION: sel_id,
CONF_SYNOLOGY_IMAGE_SIZE: size,
CONF_ALBUM_NAME: name,
}
if album_id:
data[CONF_SYNOLOGY_ALBUM_ID] = album_id
if self._syn_device_id:
data[CONF_SYNOLOGY_DEVICE_ID] = self._syn_device_id
return self.async_create_entry(title=name, data=data)
album_options = [
selector.SelectOptionDict(value="__all__", label="All photos in this space")
] + [
selector.SelectOptionDict(value=aid, label=aname)
for aid, aname in self._syn_albums.items()
]
def _multi(options: dict[str, str]):
return selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(value=v, label=l)
for v, l in options.items()
],
multiple=True,
mode=selector.SelectSelectorMode.DROPDOWN,
custom_value=False,
)
)
fields: dict[Any, Any] = {vol.Required(CONF_ALBUM_NAME): str}
# Favorites, albums and subjects are Personal-space concepts.
if self._syn_space == SYNOLOGY_SPACE_PERSONAL:
fields[vol.Optional("favorites", default=False)] = (
selector.BooleanSelector()
)
if self._syn_albums:
fields[vol.Optional("albums")] = _multi(self._syn_albums)
if self._syn_people:
fields[vol.Optional("people")] = _multi(self._syn_people)
if self._syn_places:
fields[vol.Optional("places")] = _multi(self._syn_places)
if self._syn_tags:
fields[vol.Optional("tags")] = _multi(self._syn_tags)
if self._syn_subjects:
fields[vol.Optional("subjects")] = _multi(self._syn_subjects)
fields[
vol.Optional(
CONF_SYNOLOGY_IMAGE_SIZE, default=DEFAULT_SYNOLOGY_IMAGE_SIZE
)
] = vol.In(
{
SYNOLOGY_IMAGE_LARGE: "Large (best for slideshow)",
SYNOLOGY_IMAGE_MEDIUM: "Medium",
SYNOLOGY_IMAGE_SMALL: "Small (thumbnail, fastest)",
}
)
return self.async_show_form(
step_id="synology_select", data_schema=vol.Schema(fields), errors=errors
)
async def async_step_nextcloud(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Collect and validate a Nextcloud WebDAV folder + app password."""
errors: dict[str, str] = {}
if user_input is not None:
name = user_input[CONF_ALBUM_NAME].strip()
url = user_input[CONF_NEXTCLOUD_URL].strip()
username = user_input[CONF_NEXTCLOUD_USERNAME].strip()
password = user_input.get(CONF_NEXTCLOUD_PASSWORD) or ""
folder = (user_input.get(CONF_NEXTCLOUD_FOLDER) or "").strip()
recursive = bool(user_input.get(CONF_NEXTCLOUD_RECURSIVE, False))
size = user_input.get(
CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE
)
from . import nextcloud as nc_api
client = nc_api.NextcloudClient(
self.hass, url, username, password, folder
)
try:
await client.async_validate()
except Exception: # noqa: BLE001 - any failure means bad URL/creds/folder
errors["base"] = "nextcloud_cannot_connect"
else:
await self.async_set_unique_id(
f"{DOMAIN}:{PROVIDER_NEXTCLOUD}:{client.base_url}:"
f"{username}:{client.folder}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=name,
data={
CONF_PROVIDER: PROVIDER_NEXTCLOUD,
CONF_NEXTCLOUD_URL: client.base_url,
CONF_NEXTCLOUD_USERNAME: username,
CONF_NEXTCLOUD_PASSWORD: password,
CONF_NEXTCLOUD_FOLDER: client.folder,
CONF_NEXTCLOUD_RECURSIVE: recursive,
CONF_NEXTCLOUD_IMAGE_SIZE: size,
CONF_ALBUM_NAME: name,
},
)
schema = vol.Schema(
{
vol.Required(CONF_ALBUM_NAME): str,
vol.Optional("album", default="__all__"): selector.SelectSelector(
selector.SelectSelectorConfig(
options=album_options,
mode=selector.SelectSelectorMode.DROPDOWN,
custom_value=False,
)
vol.Required(CONF_NEXTCLOUD_URL): str,
vol.Required(CONF_NEXTCLOUD_USERNAME): str,
vol.Required(CONF_NEXTCLOUD_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Optional(CONF_NEXTCLOUD_FOLDER, default=""): str,
vol.Optional(CONF_NEXTCLOUD_RECURSIVE, default=False): (
selector.BooleanSelector()
),
vol.Optional(
CONF_SYNOLOGY_IMAGE_SIZE, default=DEFAULT_SYNOLOGY_IMAGE_SIZE
CONF_NEXTCLOUD_IMAGE_SIZE, default=DEFAULT_NEXTCLOUD_IMAGE_SIZE
): vol.In(
{
SYNOLOGY_IMAGE_LARGE: "Large (best for slideshow)",
SYNOLOGY_IMAGE_MEDIUM: "Medium",
SYNOLOGY_IMAGE_SMALL: "Small (thumbnail, fastest)",
NEXTCLOUD_IMAGE_PREVIEW: "Preview (smoothest slideshow)",
NEXTCLOUD_IMAGE_ORIGINAL: "Original (full quality, slower)",
}
),
}
)
return self.async_show_form(
step_id="synology_select", data_schema=schema, errors=errors
step_id="nextcloud", data_schema=schema, errors=errors
)
@@ -58,6 +58,28 @@ PROVIDER_IMMICH = "immich"
PROVIDER_PHOTOPRISM = "photoprism"
PROVIDER_ICLOUD = "icloud"
PROVIDER_SYNOLOGY = "synology"
PROVIDER_NEXTCLOUD = "nextcloud"
# Nextcloud (authenticated WebDAV folder) provider. Points at any folder in a
# user's files and lists it over WebDAV. Auth is HTTP Basic with a username +
# app password (Settings > Security > Devices & sessions); the app password is
# stored so the coordinator can re-list on each refresh and is sent server-side
# only, never reaching the browser.
CONF_NEXTCLOUD_URL = "nextcloud_url"
CONF_NEXTCLOUD_USERNAME = "nextcloud_username"
CONF_NEXTCLOUD_PASSWORD = "nextcloud_password"
CONF_NEXTCLOUD_FOLDER = "nextcloud_folder"
CONF_NEXTCLOUD_RECURSIVE = "nextcloud_recursive"
CONF_NEXTCLOUD_IMAGE_SIZE = "nextcloud_image_size"
# ``preview`` uses the core/preview thumbnail endpoint (smoother, smaller);
# ``original`` fetches the real file straight off the WebDAV collection.
NEXTCLOUD_IMAGE_PREVIEW = "preview"
NEXTCLOUD_IMAGE_ORIGINAL = "original"
NEXTCLOUD_IMAGE_SIZE_OPTIONS = [NEXTCLOUD_IMAGE_PREVIEW, NEXTCLOUD_IMAGE_ORIGINAL]
DEFAULT_NEXTCLOUD_IMAGE_SIZE = NEXTCLOUD_IMAGE_PREVIEW
# Long edge (px) requested from the core/preview endpoint for preview quality.
NEXTCLOUD_PREVIEW_PX = 1920
# Synology Photos (direct API) provider. Talks to a DSM Photos package over its
# entry.cgi web API. The account password is stored so the coordinator can
@@ -70,6 +92,21 @@ CONF_SYNOLOGY_DEVICE_ID = "synology_device_id"
CONF_SYNOLOGY_SPACE = "synology_space"
CONF_SYNOLOGY_ALBUM_ID = "synology_album_id"
CONF_SYNOLOGY_IMAGE_SIZE = "synology_image_size"
# Passphrase for an album that was shared with the configured account. Present
# only when the chosen source is a shared-with-me album; such albums are
# reachable by passphrase rather than by album id.
CONF_SYNOLOGY_PASSPHRASE = "synology_passphrase"
# When True, the source is the account's Favorites (favorited photos) rather
# than the whole space or a specific album.
CONF_SYNOLOGY_FAVORITE = "synology_favorite"
# Composite selection: a client-side union of any mix of albums, people,
# places, tags, subjects and favorites. Synology has no OR across categories,
# so each selected member is queried on its own and the results are merged
# (see the Immich/PhotoPrism composite). Stored as a JSON object:
# ``{"favorites": bool, "album_ids": [...], "passphrases": [...],
# "person_ids": [...], "geocoding_ids": [...], "tag_ids": [...],
# "concept_ids": [...]}``; an empty composite means the whole space.
CONF_SYNOLOGY_SELECTION = "synology_selection"
# Personal ("My Photos") vs shared ("Shared Space") library.
SYNOLOGY_SPACE_PERSONAL = "personal"
+284 -44
View File
@@ -51,8 +51,20 @@ from .const import (
CONF_SYNOLOGY_SPACE,
CONF_SYNOLOGY_ALBUM_ID,
CONF_SYNOLOGY_IMAGE_SIZE,
CONF_SYNOLOGY_PASSPHRASE,
CONF_SYNOLOGY_FAVORITE,
CONF_SYNOLOGY_SELECTION,
DEFAULT_SYNOLOGY_IMAGE_SIZE,
SYNOLOGY_SPACE_PERSONAL,
CONF_NEXTCLOUD_URL,
CONF_NEXTCLOUD_USERNAME,
CONF_NEXTCLOUD_PASSWORD,
CONF_NEXTCLOUD_FOLDER,
CONF_NEXTCLOUD_RECURSIVE,
CONF_NEXTCLOUD_IMAGE_SIZE,
DEFAULT_NEXTCLOUD_IMAGE_SIZE,
NEXTCLOUD_IMAGE_ORIGINAL,
NEXTCLOUD_PREVIEW_PX,
DEFAULT_REVERSE_GEOCODE,
DOMAIN,
PROVIDER_GOOGLE_SHARED,
@@ -62,6 +74,7 @@ from .const import (
PROVIDER_PHOTOPRISM,
PROVIDER_ICLOUD,
PROVIDER_SYNOLOGY,
PROVIDER_NEXTCLOUD,
)
from .store import SlideshowStore
@@ -420,6 +433,11 @@ _NOMINATIM_TIMEOUT_S = 20
_EXIF_BATCH_SAVE = 25
_GEOCODE_BATCH_SAVE = 10
# Cap a single Nextcloud enrichment download. EXIF/IPTC/XMP live in the first
# blocks of the file, but we read the whole thing since Pillow needs a complete
# image; this bounds memory for pathological files. 64 MB matches the camera.
_NEXTCLOUD_ENRICH_MAX_BYTES = 64 * 1024 * 1024
# Inserted between background-enrichment iterations so the event loop
# stays responsive on the fast path (items that are already scanned and
# need zero work).
@@ -674,53 +692,90 @@ def _read_local_exif(path: Path) -> dict[str, Any]:
try:
with Image.open(path) as img:
exif = img.getexif()
if exif:
dt_raw = exif.get(_EXIF_TAG_DATETIME_ORIGINAL) or exif.get(
_EXIF_TAG_DATETIME
)
offset_raw = exif.get(_EXIF_TAG_OFFSET_TIME_ORIGINAL)
parsed = _parse_exif_datetime(dt_raw, offset_raw)
if parsed is not None:
out["captured_at"] = parsed
# Description can come from IPTC / XMP even when the file has no
# EXIF IFD, so this runs regardless of ``exif`` being present.
description = _read_photo_description(img, exif)
if description:
out["description"] = description
if not exif:
return out
gps = None
try:
gps = exif.get_ifd(_EXIF_TAG_GPS_IFD) or None
except Exception:
gps = None
if gps:
lat = _gps_to_decimal(
gps.get(_EXIF_GPS_LAT), gps.get(_EXIF_GPS_LAT_REF)
)
lon = _gps_to_decimal(
gps.get(_EXIF_GPS_LON), gps.get(_EXIF_GPS_LON_REF)
)
if lat is not None and lon is not None:
# Null Island guard: GPS chips and some editors stamp
# ``(0, 0)`` when the fix is invalid. Treat that as no
# location rather than dropping every such photo onto
# the equator off the African coast.
if abs(lat) < 1e-6 and abs(lon) < 1e-6:
return out
out["latitude"] = lat
out["longitude"] = lon
_read_exif_from_image(img, out)
except Exception as err:
_LOGGER.debug("EXIF: failed to read %s: %s", path, err)
return out
def _read_exif_from_image(img: Any, out: dict[str, Any]) -> None:
"""Fill ``out`` with capture date / description / GPS from an open image.
Shared by ``_read_local_exif`` (opens from a filesystem path) and
``_read_exif_from_bytes`` (opens from downloaded bytes, e.g. the
Nextcloud provider) - both hand this an already-``Image.open``'d image
plus a dict pre-seeded with a fallback ``captured_at``.
"""
exif = img.getexif()
if exif:
dt_raw = exif.get(_EXIF_TAG_DATETIME_ORIGINAL) or exif.get(
_EXIF_TAG_DATETIME
)
offset_raw = exif.get(_EXIF_TAG_OFFSET_TIME_ORIGINAL)
parsed = _parse_exif_datetime(dt_raw, offset_raw)
if parsed is not None:
out["captured_at"] = parsed
# Description can come from IPTC / XMP even when the file has no EXIF
# IFD, so this runs regardless of ``exif`` being present.
description = _read_photo_description(img, exif)
if description:
out["description"] = description
if not exif:
return
gps = None
try:
gps = exif.get_ifd(_EXIF_TAG_GPS_IFD) or None
except Exception:
gps = None
if gps:
lat = _gps_to_decimal(gps.get(_EXIF_GPS_LAT), gps.get(_EXIF_GPS_LAT_REF))
lon = _gps_to_decimal(gps.get(_EXIF_GPS_LON), gps.get(_EXIF_GPS_LON_REF))
if lat is not None and lon is not None:
# Null Island guard: GPS chips and some editors stamp ``(0, 0)``
# when the fix is invalid. Treat that as no location rather than
# dropping every such photo onto the equator off the African coast.
if abs(lat) < 1e-6 and abs(lon) < 1e-6:
return
out["latitude"] = lat
out["longitude"] = lon
def _read_exif_from_bytes(
data: bytes, mtime_fallback_ms: int | None
) -> dict[str, Any]:
"""Read EXIF metadata from already-downloaded image bytes.
Same return shape as ``_read_local_exif``, for providers (Nextcloud)
whose files live on a remote server rather than the local filesystem -
the caller downloads the file once for enrichment, regardless of which
quality is used for display. ``mtime_fallback_ms`` takes the place of
the filesystem mtime fallback (e.g. the WebDAV ``Last-Modified`` date).
"""
out: dict[str, Any] = {}
if isinstance(mtime_fallback_ms, int):
out["captured_at"] = mtime_fallback_ms
try:
from PIL import Image
except Exception: # pragma: no cover - Pillow ships with HA core
return out
try:
import io
with Image.open(io.BytesIO(data)) as img:
_read_exif_from_image(img, out)
except Exception as err:
_LOGGER.debug("EXIF: failed to read image bytes: %s", err)
return out
def _format_nominatim_location(payload: dict[str, Any]) -> str | None:
"""Turn a Nominatim reverse-geocode response into a short label.
@@ -951,6 +1006,8 @@ class AlbumCoordinator(DataUpdateCoordinator):
data = await self._update_icloud()
elif self.provider == PROVIDER_SYNOLOGY:
data = await self._update_synology()
elif self.provider == PROVIDER_NEXTCLOUD:
data = await self._update_nextcloud()
else:
raise UpdateFailed(f"Unsupported provider: {self.provider}")
except UpdateFailed:
@@ -964,7 +1021,7 @@ class AlbumCoordinator(DataUpdateCoordinator):
raise
items = data.get("items") or []
if self.provider in (PROVIDER_LOCAL_FOLDER, PROVIDER_IMMICH) and items:
if self.provider in (PROVIDER_LOCAL_FOLDER, PROVIDER_IMMICH, PROVIDER_NEXTCLOUD) and items:
# Carry forward EXIF/geocode metadata for items we've already
# scanned this session; new items get filled in by the
# background worker below.
@@ -1541,6 +1598,9 @@ class AlbumCoordinator(DataUpdateCoordinator):
device_id = self.entry.data.get(CONF_SYNOLOGY_DEVICE_ID)
space = self.entry.data.get(CONF_SYNOLOGY_SPACE, SYNOLOGY_SPACE_PERSONAL)
album_id = self.entry.data.get(CONF_SYNOLOGY_ALBUM_ID)
passphrase = self.entry.data.get(CONF_SYNOLOGY_PASSPHRASE)
favorite_only = bool(self.entry.data.get(CONF_SYNOLOGY_FAVORITE))
selection_raw = self.entry.data.get(CONF_SYNOLOGY_SELECTION)
size = self.entry.data.get(
CONF_SYNOLOGY_IMAGE_SIZE, DEFAULT_SYNOLOGY_IMAGE_SIZE
)
@@ -1557,7 +1617,23 @@ class AlbumCoordinator(DataUpdateCoordinator):
)
try:
await client.async_login()
photos = await client.async_collect_assets(album_id or None)
if selection_raw:
# Composite selection (albums + people + places + tags +
# subjects + favorites), merged client-side.
try:
selection = json.loads(selection_raw)
except (TypeError, ValueError):
selection = {}
photos = await client.async_collect_composite(selection)
else:
# Legacy single-source entries (favorites / one album / all).
photos = await client.async_collect_assets(
album_id or None,
passphrase=passphrase or None,
favorite_only=favorite_only,
)
except syn_api.SynologyPermissionError as err:
raise UpdateFailed(str(err)) from err
except Exception as err:
raise UpdateFailed(f"Error querying Synology Photos: {err}") from err
@@ -1577,10 +1653,19 @@ class AlbumCoordinator(DataUpdateCoordinator):
continue
unit_id, cache_key = ref
meta = syn_api.parse_photo_meta(p)
# Items pulled from a shared-with-me album carry their own
# passphrase (composite path); fall back to the single-album
# passphrase for legacy entries.
item_pp = p.get("_passphrase") or passphrase or None
items.append(
MediaItem(
url=syn_api.build_thumbnail_url(
client.base_url, unit_id, cache_key, size, space
client.base_url,
unit_id,
cache_key,
size,
space,
passphrase=item_pp,
),
width=meta.get("width"),
height=meta.get("height"),
@@ -1605,6 +1690,143 @@ class AlbumCoordinator(DataUpdateCoordinator):
"items": items,
}
async def _update_nextcloud(self) -> dict[str, Any]:
"""List photos from an authenticated Nextcloud WebDAV folder.
The PROPFIND listing carries filename/size/content-type/mtime but no
EXIF, so capture date, GPS and description are filled in afterwards by
the background enrichment worker (one original-file download per photo -
Nextcloud has no metadata-only endpoint the way Immich does). The app
password is sent server-side only via the coordinator's image headers.
"""
from . import nextcloud as nc_api
url = self.entry.data.get(CONF_NEXTCLOUD_URL)
username = self.entry.data.get(CONF_NEXTCLOUD_USERNAME)
password = self.entry.data.get(CONF_NEXTCLOUD_PASSWORD)
folder = self.entry.data.get(CONF_NEXTCLOUD_FOLDER) or ""
recursive = bool(self.entry.data.get(CONF_NEXTCLOUD_RECURSIVE, False))
size = self.entry.data.get(
CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE
)
if not url or not username or not password:
raise UpdateFailed("Nextcloud provider is missing URL or credentials")
client = nc_api.NextcloudClient(self.hass, url, username, password, folder)
try:
photos = await client.async_list_photos(recursive=recursive)
except Exception as err:
raise UpdateFailed(f"Error listing Nextcloud folder: {err}") from err
if not photos:
raise UpdateFailed("No images found in the Nextcloud folder")
# The camera fetches image bytes server-side with this Basic-auth
# header, so the app password never appears in the browser URL.
self.image_request_headers = dict(client.image_headers)
items: list[MediaItem] = []
for p in photos:
href = p.get("href")
if not href:
continue
if size != NEXTCLOUD_IMAGE_ORIGINAL and p.get("file_id"):
display_url = nc_api.build_preview_url(
client.base_url, p["file_id"], NEXTCLOUD_PREVIEW_PX
)
else:
display_url = href
items.append(
MediaItem(
url=display_url,
width=None,
height=None,
mime_type=p.get("content_type"),
filename=p.get("filename"),
uploaded_at=p.get("mtime_ms"),
byte_size=p.get("size"),
source_id=p.get("file_id") or href,
)
)
return {
"title": self.entry.title,
"items": items,
}
async def _enrich_nextcloud_item(self, item: MediaItem) -> None:
"""Download one Nextcloud photo's original bytes and read its EXIF.
Nextcloud's WebDAV folder has no metadata-only endpoint (unlike
Immich's per-asset detail call), so enrichment costs one full-file
download per photo regardless of the display quality configured.
"""
from . import nextcloud as nc_api
from urllib.parse import quote
url = self.entry.data.get(CONF_NEXTCLOUD_URL)
username = self.entry.data.get(CONF_NEXTCLOUD_USERNAME)
password = self.entry.data.get(CONF_NEXTCLOUD_PASSWORD)
folder = self.entry.data.get(CONF_NEXTCLOUD_FOLDER) or ""
if not username or not password or not url:
item.exif_scanned = True
return
# Reconstruct the original-file URL: for preview items the display url
# is the preview endpoint, so fall back to the folder href by filename.
original_url = None
if isinstance(item.url, str) and "/remote.php/dav/files/" in item.url:
original_url = item.url
elif item.filename:
client = nc_api.NextcloudClient(self.hass, url, username, password, folder)
original_url = client.dav_root + quote(item.filename)
if not original_url:
item.exif_scanned = True
return
headers = {
"Authorization": nc_api.basic_auth_header(username, password)
}
session = async_get_clientsession(self.hass)
try:
async with async_timeout.timeout(30):
async with session.get(original_url, headers=headers) as resp:
resp.raise_for_status()
chunks: list[bytes] = []
total = 0
async for chunk in resp.content.iter_chunked(64 * 1024):
total += len(chunk)
if total > _NEXTCLOUD_ENRICH_MAX_BYTES:
_LOGGER.debug(
"Nextcloud: %s exceeded %d byte enrichment cap; skipping",
item.filename, _NEXTCLOUD_ENRICH_MAX_BYTES,
)
item.exif_scanned = True
return
chunks.append(chunk)
data = b"".join(chunks)
except asyncio.CancelledError:
raise
except Exception as err: # noqa: BLE001
_LOGGER.debug(
"Nextcloud: failed to download %s for enrichment: %s",
item.filename, err,
)
item.exif_scanned = True
return
info = await self.hass.async_add_executor_job(
_read_exif_from_bytes, data, item.uploaded_at
)
if "captured_at" in info:
item.captured_at = info["captured_at"]
if "description" in info:
item.description = info["description"]
if "latitude" in info and "longitude" in info:
item.latitude = info["latitude"]
item.longitude = info["longitude"]
item.exif_scanned = True
async def _enrich_immich_item(self, item: MediaItem) -> None:
"""Fetch one Immich asset's detail and fill location/description."""
from . import immich as immich_api
@@ -1677,6 +1899,24 @@ class AlbumCoordinator(DataUpdateCoordinator):
self.async_set_updated_data(data)
continue
if self.provider == PROVIDER_NEXTCLOUD:
try:
await self._enrich_nextcloud_item(item)
except asyncio.CancelledError:
raise
except Exception as err: # noqa: BLE001
_LOGGER.debug("Nextcloud 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
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/eyalgal/album_slideshow/issues",
"requirements": ["Pillow"],
"version": "1.5.0"
"version": "1.6.0"
}
+25 -5
View File
@@ -87,7 +87,7 @@
},
"synology": {
"title": "Synology Photos",
"description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Tip: use a dedicated read-only Photos account rather than an admin login.",
"description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Choose Personal for your own photos and albums (this is also where albums shared with you appear); choose Shared Space only if your NAS has the shared team library enabled. Tip: use a dedicated Photos account rather than an admin login.",
"data": {
"synology_url": "DSM URL",
"synology_username": "Username",
@@ -98,12 +98,30 @@
},
"synology_select": {
"title": "Synology source",
"description": "Pick an album or show the whole library, and choose the image quality.",
"description": "Choose what to show. Tick any mix of favorites, albums, people, places, tags and subjects; they are combined into one slideshow. Leave everything empty to show all photos in this space.",
"data": {
"album_name": "Album name",
"album": "Album",
"favorites": "Favorites",
"albums": "Albums",
"people": "People",
"places": "Places",
"tags": "Tags",
"subjects": "Subjects",
"synology_image_size": "Image quality"
}
},
"nextcloud": {
"title": "Nextcloud folder",
"description": "Connect to a folder in your Nextcloud files over WebDAV. Enter the server address (e.g. http://192.168.1.10, or your Nextcloud domain over HTTPS), your username, and an app password (create one under Settings > Security > Devices and sessions - not your main login password). Point it at a folder path (e.g. Photos/Family), or leave the folder blank for your whole files root. Turn on recursive to include subfolders.",
"data": {
"album_name": "Album name",
"nextcloud_url": "Server URL",
"nextcloud_username": "Username",
"nextcloud_password": "App password",
"nextcloud_folder": "Folder path (optional)",
"nextcloud_recursive": "Include subfolders",
"nextcloud_image_size": "Image quality"
}
}
},
"error": {
@@ -122,13 +140,15 @@
"invalid_icloud_url": "That does not look like an iCloud Shared Album link.",
"icloud_cannot_connect": "Could not reach that album. Check the link is a current public share.",
"synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.",
"synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue."
"synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue.",
"synology_shared_unavailable": "Could not access the Shared Space. Enable Shared Space in Synology Photos and make sure this account has access to it, or choose Personal instead.",
"nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path."
}
},
"options": {
"step": {
"init": {
"title": "Local Folder options",
"title": "Location & privacy options",
"description": "Reverse geocoding sends your photos' EXIF GPS coordinates to the public OpenStreetMap Nominatim service to look up a human-readable place name. Coordinates are rounded to ~100 m before lookup and cached on disk. Turn this off to keep coordinates entirely local; the latitude and longitude attributes still work either way.",
"data": {
"reverse_geocode": "Reverse-geocode EXIF GPS coordinates via OpenStreetMap"
@@ -87,7 +87,7 @@
},
"synology": {
"title": "Synology Photos",
"description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Tip: use a dedicated read-only Photos account rather than an admin login.",
"description": "Connect to the Photos package on your Synology NAS. Enter the DSM address (e.g. http://192.168.1.10:5000 or your HTTPS/QuickConnect URL) and an account. If the account has two-factor authentication, also enter a current 6-digit code once; a trusted-device token is then stored so future refreshes do not need a code. Choose Personal for your own photos and albums (this is also where albums shared with you appear); choose Shared Space only if your NAS has the shared team library enabled. Tip: use a dedicated Photos account rather than an admin login.",
"data": {
"synology_url": "DSM URL",
"synology_username": "Username",
@@ -98,12 +98,30 @@
},
"synology_select": {
"title": "Synology source",
"description": "Pick an album or show the whole library, and choose the image quality.",
"description": "Choose what to show. Tick any mix of favorites, albums, people, places, tags and subjects; they are combined into one slideshow. Leave everything empty to show all photos in this space.",
"data": {
"album_name": "Album name",
"album": "Album",
"favorites": "Favorites",
"albums": "Albums",
"people": "People",
"places": "Places",
"tags": "Tags",
"subjects": "Subjects",
"synology_image_size": "Image quality"
}
},
"nextcloud": {
"title": "Nextcloud folder",
"description": "Connect to a folder in your Nextcloud files over WebDAV. Enter the server address (e.g. http://192.168.1.10, or your Nextcloud domain over HTTPS), your username, and an app password (create one under Settings > Security > Devices and sessions - not your main login password). Point it at a folder path (e.g. Photos/Family), or leave the folder blank for your whole files root. Turn on recursive to include subfolders.",
"data": {
"album_name": "Album name",
"nextcloud_url": "Server URL",
"nextcloud_username": "Username",
"nextcloud_password": "App password",
"nextcloud_folder": "Folder path (optional)",
"nextcloud_recursive": "Include subfolders",
"nextcloud_image_size": "Image quality"
}
}
},
"error": {
@@ -122,13 +140,15 @@
"invalid_icloud_url": "That does not look like an iCloud Shared Album link.",
"icloud_cannot_connect": "Could not reach that album. Check the link is a current public share.",
"synology_cannot_connect": "Could not connect to Synology. Check the URL, username and password.",
"synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue."
"synology_otp_required": "This account needs a two-factor code. Enter a current 6-digit code to continue.",
"synology_shared_unavailable": "Could not access the Shared Space. Enable Shared Space in Synology Photos and make sure this account has access to it, or choose Personal instead.",
"nextcloud_cannot_connect": "Could not connect to Nextcloud. Check the URL, username, app password and folder path."
}
},
"options": {
"step": {
"init": {
"title": "Local Folder options",
"title": "Location & privacy options",
"description": "Reverse geocoding sends your photos' EXIF GPS coordinates to the public OpenStreetMap Nominatim service to look up a human-readable place name. Coordinates are rounded to ~100 m before lookup and cached on disk. Turn this off to keep coordinates entirely local; the latitude and longitude attributes still work either way.",
"data": {
"reverse_geocode": "Reverse-geocode EXIF GPS coordinates via OpenStreetMap"
@@ -26,7 +26,7 @@
* tap_action: none # none | more-info
*/
const VERSION = "1.5.0";
const VERSION = "1.6.0";
const ANIMATED_TRANSITIONS = [
"fade",
+93 -5
View File
@@ -32,13 +32,16 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from midealocal.device import DeviceType, MideaDevice, ProtocolVersion
from midealocal.devices import device_selector
from midealocal.discover import discover
from .const import (
ALL_PLATFORM,
CONF_ACCOUNT,
CONF_KEY,
CONF_MAC,
CONF_MODEL,
CONF_REFRESH_INTERVAL,
CONF_SN,
CONF_SUBTYPE,
DEVICES,
DOMAIN,
@@ -76,7 +79,7 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non
dev.set_refresh_interval(refresh_interval)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ARG001
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # ruff:ignore[unused-function-argument]
"""Set up midea_lan component when load this integration.
Returns
@@ -91,13 +94,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
"dict",
device_entities["entities"],
).items():
attribute_key = (
attribute_name
if isinstance(attribute_name, str)
else attribute_name.value
)
if (
attribute.get("type") in EXTRA_SWITCH
and attribute_name.value not in attributes
and attribute_key not in attributes
):
attributes.append(attribute_name.value)
attributes.append(attribute_key)
def service_set_attribute(service: Any) -> None: # noqa: ANN401
def service_set_attribute(service: Any) -> None: # ruff:ignore[any-type]
"""Set service attribute func."""
device_id: int = service.data["device_id"]
attr = service.data["attribute"]
@@ -126,7 +134,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
attr,
)
def service_send_command(service: Any) -> None: # noqa: ANN401
def service_send_command(service: Any) -> None: # ruff:ignore[any-type]
"""Send command to service func."""
device_id = service.data.get("device_id")
cmd_type = service.data.get("cmd_type")
@@ -204,6 +212,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
subtype = config_entry.data.get(CONF_SUBTYPE, 0)
protocol: ProtocolVersion = ProtocolVersion(config_entry.data[CONF_PROTOCOL])
customize: str = config_entry.options.get(CONF_CUSTOMIZE, "")
mac: str | None = config_entry.data.get(CONF_MAC)
serial_number: str | None = config_entry.data.get(CONF_SN)
if protocol == ProtocolVersion.V3 and (key == "" or token == ""):
_LOGGER.error("For V3 devices, the key and the token is required")
return False
@@ -223,6 +233,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
model,
subtype,
customize,
mac,
serial_number,
)
# hass core version < 2024.3
else:
@@ -238,6 +250,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
model=model,
subtype=subtype,
customize=customize,
mac=mac,
serial_number=serial_number,
)
if device:
if refresh_interval is not None:
@@ -306,6 +320,80 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
_LOGGER.debug("Migration to configuration version 2 successful")
# 2.1 -> 2.2: backfill mac address and serial number for existing devices
entry_version_2 = 2
if config_entry.version == entry_version_2 and config_entry.minor_version == 1:
_LOGGER.debug("Migrating configuration from version 2.1")
if await _async_backfill_mac_and_sn(hass, config_entry):
if (MAJOR_VERSION, MINOR_VERSION) >= (2024, 3):
hass.config_entries.async_update_entry(config_entry, minor_version=2)
else:
config_entry.minor_version = 2
hass.config_entries.async_update_entry(config_entry)
_LOGGER.debug("Migration to configuration version 2.2 successful")
else:
_LOGGER.debug(
"Device %s did not respond to discovery;"
" mac/serial number migration will be retried on next start",
config_entry.data.get(CONF_DEVICE_ID),
)
return True
async def _async_backfill_mac_and_sn(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
"""Best-effort discovery to backfill mac address and serial number.
Returns
-------
True if the entry already had the data, has no IP/device id to look up,
or was successfully updated. False if the device did not answer
discovery and the migration should be retried on a later start.
"""
if config_entry.data.get(CONF_TYPE) == CONF_ACCOUNT:
return True
if config_entry.data.get(CONF_MAC) or config_entry.data.get(CONF_SN):
return True
# Honor an IP override set via the options flow (e.g. after a DHCP change),
# mirroring async_setup_entry; the data IP may be stale.
ip_address = config_entry.options.get(CONF_IP_ADDRESS)
if ip_address is None:
ip_address = config_entry.data.get(CONF_IP_ADDRESS)
device_id = config_entry.data.get(CONF_DEVICE_ID)
if ip_address is None or device_id is None:
return True
try:
found_devices = await hass.async_add_executor_job(
lambda: discover(ip_address=ip_address),
)
except Exception:
# Best-effort migration: any discovery failure (socket error, malformed
# reply from a legacy device, parse error) must not break entry setup.
# Retry on the next start instead.
_LOGGER.debug(
"Discovery for device %s failed; mac/serial number migration"
" will be retried on next start",
device_id,
exc_info=True,
)
return False
device = found_devices.get(int(device_id))
if device is None:
return False
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
CONF_MAC: device.get(CONF_MAC),
CONF_SN: device.get(CONF_SN),
},
)
return True
@@ -30,9 +30,15 @@ async def async_setup_entry(
"dict",
MIDEA_DEVICES[device.device_type]["entities"],
).items():
if config["type"] == Platform.BINARY_SENSOR and entity_key in extra_sensors:
sensor = MideaBinarySensor(device, entity_key)
binary_sensors.append(sensor)
if config["type"] != Platform.BINARY_SENSOR or entity_key not in extra_sensors:
continue
required_attribute = config.get("required_attribute")
if (
required_attribute is not None
and required_attribute not in device.attributes
):
continue
binary_sensors.append(MideaBinarySensor(device, entity_key))
async_add_entities(binary_sensors)
@@ -41,10 +47,10 @@ class MideaBinarySensor(MideaEntity, BinarySensorEntity):
@property
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return device class."""
"""Device class of the binary sensor."""
return cast("BinarySensorDeviceClass", self._config.get("device_class"))
@property
def is_on(self) -> bool:
"""Return true if sensor state is on."""
"""Whether the binary sensor is on."""
return cast("bool", self._device.get_attribute(self._entity_key))
+231 -11
View File
@@ -1,5 +1,6 @@
"""Midea Climate entries."""
import json
import logging
from typing import Any, ClassVar, cast
@@ -27,6 +28,7 @@ from homeassistant.components.climate import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_CUSTOMIZE,
CONF_DEVICE_ID,
CONF_SWITCHES,
MAJOR_VERSION,
@@ -179,15 +181,15 @@ class MideaClimate(MideaEntity, ClimateEntity):
"""Midea Climate extra state attributes."""
return cast("dict", self._device.attributes)
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Climate turn on."""
self._device.set_attribute(attr="power", value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Climate turn off."""
self._device.set_attribute(attr="power", value=False)
def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea Climate set temperature."""
if ATTR_TEMPERATURE not in kwargs:
return
@@ -241,7 +243,7 @@ class MideaClimate(MideaEntity, ClimateEntity):
elif old_mode == PRESET_BOOST:
self._device.set_attribute(attr="boost_mode", value=False)
def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002
def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Climate update state."""
if not self.hass:
_LOGGER.warning(
@@ -250,7 +252,7 @@ class MideaClimate(MideaEntity, ClimateEntity):
type(self),
)
return
self.schedule_update_ha_state()
self.schedule_update_if_running()
class MideaACClimate(MideaClimate):
@@ -266,7 +268,9 @@ class MideaACClimate(MideaClimate):
) -> None:
"""Midea AC Climate entity init."""
super().__init__(device, entity_key)
self._attr_hvac_modes = [
# fixed positional map: device "mode" int -> HVACMode. DO NOT reorder,
# the device mode value indexes into this list.
self._mode_index = [
HVACMode.OFF,
HVACMode.AUTO,
HVACMode.COOL,
@@ -274,6 +278,14 @@ class MideaACClimate(MideaClimate):
HVACMode.HEAT,
HVACMode.FAN_ONLY,
]
self._attr_hvac_modes = list(self._mode_index)
# customize overrides parsed once; B5 capabilities read dynamically
# (they arrive after the first refresh, so hvac_modes/supported_features
# are computed lazily — priority: customize > B5 > defaults)
self._customize_swing: bool | None = None
self._customize_hvac_modes: list[HVACMode] | None = None
self._customize_preset_modes: list[str] | None = None
self._parse_capability_customize(config_entry)
self._fan_speeds: dict[str, int] = {
FAN_SILENT: 20,
FAN_LOW: 40,
@@ -304,6 +316,176 @@ class MideaACClimate(MideaClimate):
and "indoor_humidity" in config_entry.options["sensors"]
)
def _parse_capability_customize(self, config_entry: ConfigEntry) -> None:
"""Parse the swing / hvac_modes customize overrides (highest priority).
``{"swing": false, "hvac_modes": ["off", "cool", "dry", "fan_only"]}``
"""
customize = config_entry.options.get(CONF_CUSTOMIZE, "")
if not customize:
return
try:
params = json.loads(customize)
except (ValueError, TypeError):
return
if not isinstance(params, dict):
return
if "swing" in params:
self._customize_swing = bool(params["swing"])
modes = params.get("hvac_modes")
if isinstance(modes, list):
valid = {mode.value: mode for mode in self._mode_index}
wanted: list[HVACMode] = []
for name in modes:
hvac = valid.get(str(name))
if hvac is not None and hvac not in wanted:
wanted.append(hvac)
if HVACMode.OFF not in wanted:
wanted.insert(0, HVACMode.OFF)
if any(hvac != HVACMode.OFF for hvac in wanted):
self._customize_hvac_modes = wanted
presets = params.get("preset_modes")
if isinstance(presets, list):
valid_presets = {
PRESET_NONE,
PRESET_COMFORT,
PRESET_ECO,
PRESET_BOOST,
PRESET_SLEEP,
PRESET_AWAY,
}
wanted_p: list[str] = []
for name in presets:
preset = str(name)
if preset in valid_presets and preset not in wanted_p:
wanted_p.append(preset)
if PRESET_NONE not in wanted_p:
wanted_p.insert(0, PRESET_NONE)
# honor even an empty / none-only list (user wants no presets)
self._customize_preset_modes = wanted_p
def _capability_swing(self) -> bool:
"""Whether swing is available (customize > B5 capability > default).
Returns:
``True`` when the swing control should be exposed.
"""
if self._customize_swing is not None:
return self._customize_swing
caps = getattr(self._device, "capabilities", {})
if "swing_vertical" in caps or "swing_horizontal" in caps:
return bool(caps.get("swing_vertical") or caps.get("swing_horizontal"))
return True
@property
def hvac_modes(self) -> list[HVACMode]:
"""hvac_modes: customize > B5 capabilities > default full set.
Read dynamically so capabilities decoded after the first refresh are
reflected (they are not yet available when the entity is created).
"""
if self._customize_hvac_modes is not None:
return self._customize_hvac_modes
caps = getattr(self._device, "capabilities", {})
if not caps:
return list(self._mode_index)
modes = [HVACMode.OFF]
if caps.get("auto_mode"):
modes.append(HVACMode.AUTO)
if caps.get("cool_mode"):
modes.append(HVACMode.COOL)
if caps.get("dry_mode"):
modes.append(HVACMode.DRY)
if caps.get("heat_mode"):
modes.append(HVACMode.HEAT)
# fan-only is always available on AC devices
modes.append(HVACMode.FAN_ONLY)
return modes
@property
def preset_modes(self) -> list[str]:
"""preset_modes: customize > B5 capabilities > default full set.
B5 only declares eco and turbo (and heat implies away). comfort and
sleep have no capability flag, so they are kept unless overridden via
the customize ``preset_modes`` option.
"""
all_presets = [
PRESET_NONE,
PRESET_COMFORT,
PRESET_ECO,
PRESET_BOOST,
PRESET_SLEEP,
PRESET_AWAY,
]
if self._customize_preset_modes is not None:
return self._customize_preset_modes
caps = getattr(self._device, "capabilities", {})
if not caps:
return all_presets
keep = {
PRESET_NONE: True,
PRESET_COMFORT: True,
PRESET_ECO: bool(caps.get("eco")),
PRESET_BOOST: bool(caps.get("turbo_cool") or caps.get("turbo_heat")),
PRESET_SLEEP: True,
PRESET_AWAY: bool(caps.get("heat_mode")),
}
return [preset for preset in all_presets if keep[preset]]
@property
def supported_features(self) -> ClimateEntityFeature:
"""Midea AC Climate supported features."""
features = super().supported_features
if not self._capability_swing():
features &= ~ClimateEntityFeature.SWING_MODE
# drop the preset control entirely when no real preset is available
if not any(preset != PRESET_NONE for preset in self.preset_modes):
features &= ~ClimateEntityFeature.PRESET_MODE
return features
@property
def hvac_mode(self) -> HVACMode:
"""Midea AC Climate hvac mode (device mode int -> fixed map)."""
if self._device.get_attribute("power"):
mode = cast("int", self._device.get_attribute("mode"))
return self._mode_index[mode]
return HVACMode.OFF
def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Midea AC Climate set hvac mode (via the fixed map index)."""
if hvac_mode == HVACMode.OFF:
self.turn_off()
else:
self._device.set_attribute(
attr="mode",
value=self._mode_index.index(hvac_mode),
)
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea AC Climate set temperature (raw mode via the fixed map).
``hvac_modes`` is a filtered subset here, so the raw protocol mode must
come from the fixed ``_mode_index`` and not from the displayed list.
"""
if ATTR_TEMPERATURE not in kwargs:
return
temperature = float(int((float(kwargs[ATTR_TEMPERATURE]) * 2) + 0.5)) / 2
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
if hvac_mode == HVACMode.OFF:
self.turn_off()
else:
try:
mode = self._mode_index.index(hvac_mode.lower()) if hvac_mode else None
self._device.set_target_temperature(
target_temperature=temperature,
mode=mode,
zone=None,
)
except ValueError:
_LOGGER.exception("Error setting temperature with: %s", kwargs)
@property
def fan_mode(self) -> str:
"""Midea AC Climate fan mode."""
@@ -320,6 +502,28 @@ class MideaACClimate(MideaClimate):
return str(FAN_LOW)
return str(FAN_SILENT)
@property
def fan_modes(self) -> list[str] | None:
"""fan_modes restricted to the speeds the device reports (B5).
Falls back to the full set when no capability is reported.
"""
caps = getattr(self._device, "capabilities", {})
if not caps:
return list(self._fan_speeds.keys())
cap_by_fan = {
FAN_SILENT: "fan_silent",
FAN_LOW: "fan_low",
FAN_MEDIUM: "fan_medium",
FAN_HIGH: "fan_high",
FAN_FULL_SPEED: "fan_custom",
FAN_AUTO: "fan_auto",
}
modes = [
name for name in self._fan_speeds if caps.get(cap_by_fan.get(name, ""))
]
return modes or list(self._fan_speeds.keys())
@property
def target_temperature_step(self) -> float:
"""Midea AC Climate target temperature step."""
@@ -337,7 +541,7 @@ class MideaACClimate(MideaClimate):
@property
def current_humidity(self) -> float | None:
"""Return the current indoor humidity, or None if unavailable."""
"""Current indoor humidity, or None if unavailable."""
# fix error humidity, disable indoor_humidity in web UI
# https://github.com/wuwentao/midea_ac_lan/pull/641
if not self._indoor_humidity_enabled:
@@ -357,6 +561,22 @@ class MideaACClimate(MideaClimate):
self._device.get_attribute(ACAttributes.outdoor_temperature),
)
@property
def min_temp(self) -> float:
"""Midea AC Climate min temperature (from device capability)."""
value = self._device.get_attribute(ACAttributes.min_temperature)
if value is None:
return TEMPERATURE_MIN
return cast("float", value)
@property
def max_temp(self) -> float:
"""Midea AC Climate max temperature (from device capability)."""
value = self._device.get_attribute(ACAttributes.max_temperature)
if value is None:
return TEMPERATURE_MAX
return cast("float", value)
def set_fan_mode(self, fan_mode: str) -> None:
"""Midea AC Climate set fan mode."""
fan_speed = self._fan_speeds.get(fan_mode)
@@ -572,11 +792,11 @@ class MideaC3Climate(MideaClimate):
self._temperature(False)[self._zone],
)
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea C3 Climate turn on."""
self._device.set_attribute(attr=self._power_attr, value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea C3 Climate turn off."""
self._device.set_attribute(attr=self._power_attr, value=False)
@@ -605,7 +825,7 @@ class MideaC3Climate(MideaClimate):
"""Midea C3 Climate current temperature."""
return None
def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea C3 Climate set temperature."""
if ATTR_TEMPERATURE not in kwargs:
return
@@ -681,7 +901,7 @@ class MideaFBClimate(MideaClimate):
self._device.get_attribute(FBAttributes.current_temperature),
)
def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea FB Climate set temperature."""
if ATTR_TEMPERATURE not in kwargs:
return
+33 -2
View File
@@ -70,13 +70,17 @@ else:
from .const import (
CONF_ACCOUNT,
CONF_KEY,
CONF_MAC,
CONF_MODEL,
CONF_REFRESH_INTERVAL,
CONF_SERVER,
CONF_SN,
CONF_SUBTYPE,
DEVICES,
DOMAIN,
EXTRA_CONTROL,
EXTRA_SENSOR,
supports_model,
)
from .midea_devices import MIDEA_DEVICES
@@ -105,7 +109,7 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
"""
VERSION = 2
MINOR_VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""MideaLanConfigFlow class."""
@@ -140,7 +144,7 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
record_file = storage_path.joinpath(f"{data[CONF_DEVICE_ID]!s}.json")
save_json(str(record_file), data)
def _load_device_config(self, device_id: str) -> Any: # noqa: ANN401
def _load_device_config(self, device_id: str) -> Any: # ruff:ignore[any-type]
"""Load device config from json file with device id.
Returns
@@ -798,6 +802,8 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
CONF_SUBTYPE: user_input[CONF_SUBTYPE],
CONF_TOKEN: user_input[CONF_TOKEN],
CONF_KEY: user_input[CONF_KEY],
CONF_MAC: device.get(CONF_MAC),
CONF_SN: device.get(CONF_SN),
}
# save device json config when adding new device
self._save_device_config(data)
@@ -937,13 +943,38 @@ class MideaLanOptionsFlowHandler(OptionsFlow):
return self.async_create_entry(title="", data=user_input)
sensors = {}
switches = {}
device_id = self._config_entry.data.get(CONF_DEVICE_ID)
device = (
self.hass.data.get(DOMAIN, {}).get(DEVICES, {}).get(device_id)
if device_id is not None
else None
)
selected_attributes = set(
self._config_entry.options.get(CONF_SENSORS, []),
) | set(self._config_entry.options.get(CONF_SWITCHES, []))
for attribute, attribute_config in cast(
"dict",
MIDEA_DEVICES[cast("int", self._device_type)]["entities"],
).items():
if not supports_model(
self._config_entry.data.get(CONF_MODEL),
attribute_config,
):
continue
attribute_name = (
attribute if isinstance(attribute, str) else attribute.value
)
required_attribute = attribute_config.get("required_attribute")
if (
required_attribute is not None
and (
device is None
or required_attribute not in device.attributes
or device.get_attribute(required_attribute) is None
)
and attribute_name not in selected_attributes
):
continue
if attribute_config.get("type") in EXTRA_SENSOR:
sensors[attribute_name] = attribute_config.get("name")
elif attribute_config.get(
+23 -1
View File
@@ -1,6 +1,7 @@
"""Const for Midea Lan."""
from enum import IntEnum
from typing import Any, cast
from homeassistant.const import Platform
@@ -14,10 +15,19 @@ CONF_SUBTYPE = "subtype"
CONF_ACCOUNT = "account"
CONF_SERVER = "server"
CONF_REFRESH_INTERVAL = "refresh_interval"
CONF_MAC = "mac"
CONF_SN = "sn"
EXTRA_SENSOR = [Platform.SENSOR, Platform.BINARY_SENSOR]
EXTRA_SWITCH = [Platform.SWITCH, Platform.LOCK, Platform.SELECT, Platform.NUMBER]
EXTRA_SWITCH = [
Platform.SWITCH,
Platform.LOCK,
Platform.SELECT,
Platform.NUMBER,
Platform.TIME,
]
EXTRA_CONTROL = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.WATER_HEATER,
Platform.FAN,
@@ -28,6 +38,18 @@ EXTRA_CONTROL = [
ALL_PLATFORM = EXTRA_SENSOR + EXTRA_CONTROL
def supports_model(model: object, config: dict[str, Any]) -> bool:
"""Return if the entity config applies to the device model.
Returns
-------
True if the entity is available for the device model.
"""
models = config.get("models")
return not models or str(model) in cast("list[str]", models)
class FanSpeed(IntEnum):
"""FanSpeed reference values."""
@@ -19,7 +19,7 @@ TO_REDACT = {CONF_TOKEN, CONF_KEY}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, # noqa: ARG001
hass: HomeAssistant, # ruff:ignore[unused-function-argument]
entry: ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry.
+80 -25
View File
@@ -23,7 +23,13 @@ from midealocal.devices.x40 import DeviceAttributes as X40Attributes
from midealocal.devices.x40 import MideaX40Device
from .const import DEVICES, DOMAIN
from .midea_devices import MIDEA_DEVICES
from .midea_devices import (
FRESH_AIR_EXHAUST,
FRESH_AIR_EXHAUST_MODE,
FRESH_AIR_EXHAUST_POWER,
FRESH_AIR_EXHAUST_SPEED,
MIDEA_DEVICES,
)
from .midea_entity import MideaEntity
_LOGGER = logging.getLogger(__name__)
@@ -45,6 +51,12 @@ async def async_setup_entry(
"dict",
MIDEA_DEVICES[device.device_type]["entities"],
).items():
required_attribute = config.get("required_attribute")
if (
required_attribute is not None
and required_attribute not in device.attributes
):
continue
if config["type"] == Platform.FAN and (
config.get("default") or entity_key in extra_switches
):
@@ -100,7 +112,7 @@ class MideaFan(MideaEntity, FanEntity):
"""Midea Fan fan speed."""
return cast("int", self._device.get_attribute("fan_speed"))
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Fan turn off."""
self._device.set_attribute(attr="power", value=False)
@@ -117,7 +129,7 @@ class MideaFan(MideaEntity, FanEntity):
"""Midea Fan percentage."""
if not self.fan_speed:
return None
return int(round(self.fan_speed * self.percentage_step)) # noqa: RUF046
return int(round(self.fan_speed * self.percentage_step)) # ruff:ignore[unnecessary-cast-to-int]
def set_percentage(self, percentage: int) -> None:
"""Midea Fan set percentage."""
@@ -131,7 +143,7 @@ class MideaFan(MideaEntity, FanEntity):
else:
await self.hass.async_add_executor_job(self.set_percentage, percentage)
def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002
def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Fan update state."""
if not self.hass:
_LOGGER.warning(
@@ -140,7 +152,7 @@ class MideaFan(MideaEntity, FanEntity):
type(self),
)
return
self.schedule_update_ha_state()
self.schedule_update_if_running()
class MideaFAFan(MideaFan):
@@ -163,7 +175,7 @@ class MideaFAFan(MideaFan):
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any, # noqa: ANN401, ARG002
**kwargs: Any, # ruff:ignore[any-type, unused-method-argument]
) -> None:
"""Midea FA Fan turn on."""
fan_speed = int(percentage / self.percentage_step + 0.5) if percentage else None
@@ -189,7 +201,7 @@ class MideaB6Fan(MideaFan):
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any, # noqa: ANN401, ARG002
**kwargs: Any, # ruff:ignore[any-type, unused-method-argument]
) -> None:
"""Midea B6 Fan turn on."""
fan_speed = int(percentage / self.percentage_step + 0.5) if percentage else None
@@ -204,6 +216,7 @@ class MideaACFreshAirFan(MideaFan):
def __init__(self, device: MideaACDevice, entity_key: str) -> None:
"""Midea AC Fresh Air Fan entity init."""
super().__init__(device, entity_key)
self._is_exhaust = entity_key == FRESH_AIR_EXHAUST
self._attr_supported_features = (
FanEntityFeature.SET_SPEED
| FanEntityFeature.PRESET_MODE
@@ -214,47 +227,89 @@ class MideaACFreshAirFan(MideaFan):
@property
def preset_modes(self) -> list[str] | None:
"""Midea AC Fan preset modes."""
if self._is_exhaust:
return cast(
"list[str]",
getattr(self._device, "fresh_air_exhaust_fan_speeds", []),
)
return cast("list", self._device.fresh_air_fan_speeds)
@property
def is_on(self) -> bool:
"""Midea AC Fan is on."""
return cast("bool", self._device.get_attribute(ACAttributes.fresh_air_power))
attribute = (
FRESH_AIR_EXHAUST_POWER
if self._is_exhaust
else ACAttributes.fresh_air_power
)
return cast("bool", self._device.get_attribute(attribute))
@property
def fan_speed(self) -> int:
"""Midea AC Fan fan speed."""
return cast("int", self._device.get_attribute(ACAttributes.fresh_air_fan_speed))
attribute = (
FRESH_AIR_EXHAUST_SPEED
if self._is_exhaust
else ACAttributes.fresh_air_fan_speed
)
return cast("int", self._device.get_attribute(attribute))
def turn_on(
self,
percentage: int | None = None, # noqa: ARG002
preset_mode: str | None = None, # noqa: ARG002
**kwargs: Any, # noqa: ANN401, ARG002
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any, # ruff:ignore[any-type, unused-method-argument]
) -> None:
"""Midea AC Fan tun on."""
self._device.set_attribute(attr=ACAttributes.fresh_air_power, value=True)
if preset_mode is not None:
self.set_preset_mode(preset_mode)
return
if percentage is not None:
self.set_percentage(percentage)
return
attribute = (
FRESH_AIR_EXHAUST_POWER
if self._is_exhaust
else ACAttributes.fresh_air_power
)
self._device.set_attribute(attr=attribute, value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea AC Fan turn off."""
self._device.set_attribute(attr=ACAttributes.fresh_air_power, value=False)
attribute = (
FRESH_AIR_EXHAUST_POWER
if self._is_exhaust
else ACAttributes.fresh_air_power
)
self._device.set_attribute(attr=attribute, value=False)
def set_percentage(self, percentage: int) -> None:
"""Midea AC Fan set percentage."""
fan_speed = int(percentage / self.percentage_step + 0.5)
attribute = (
FRESH_AIR_EXHAUST_SPEED
if self._is_exhaust
else ACAttributes.fresh_air_fan_speed
)
self._device.set_attribute(
attr=ACAttributes.fresh_air_fan_speed,
attr=attribute,
value=fan_speed,
)
def set_preset_mode(self, preset_mode: str) -> None:
"""Midea AC Fan set preset mode."""
self._device.set_attribute(attr=ACAttributes.fresh_air_mode, value=preset_mode)
attribute = (
FRESH_AIR_EXHAUST_MODE if self._is_exhaust else ACAttributes.fresh_air_mode
)
self._device.set_attribute(attr=attribute, value=preset_mode)
@property
def preset_mode(self) -> str | None:
"""Midea AC Fan preset mode."""
return cast("str", self._device.get_attribute(attr=ACAttributes.fresh_air_mode))
attribute = (
FRESH_AIR_EXHAUST_MODE if self._is_exhaust else ACAttributes.fresh_air_mode
)
return cast("str", self._device.get_attribute(attr=attribute))
class MideaCEFan(MideaFan):
@@ -274,9 +329,9 @@ class MideaCEFan(MideaFan):
def turn_on(
self,
percentage: int | None = None, # noqa: ARG002
preset_mode: str | None = None, # noqa: ARG002
**kwargs: Any, # noqa: ANN401, ARG002
percentage: int | None = None, # ruff:ignore[unused-method-argument]
preset_mode: str | None = None, # ruff:ignore[unused-method-argument]
**kwargs: Any, # ruff:ignore[any-type, unused-method-argument]
) -> None:
"""Midea CE Fan turn on."""
self._device.set_attribute(attr=CEAttributes.power, value=True)
@@ -308,13 +363,13 @@ class MideaX40Fan(MideaFan):
def turn_on(
self,
percentage: int | None = None, # noqa: ARG002
preset_mode: str | None = None, # noqa: ARG002
**kwargs: Any, # noqa: ANN401, ARG002
percentage: int | None = None, # ruff:ignore[unused-method-argument]
preset_mode: str | None = None, # ruff:ignore[unused-method-argument]
**kwargs: Any, # ruff:ignore[any-type, unused-method-argument]
) -> None:
"""Midea X40 Fan turn on."""
self._device.set_attribute(attr=X40Attributes.fan_speed, value=1)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea X40 Fan turn off."""
self._device.set_attribute(attr=X40Attributes.fan_speed, value=0)
+4 -4
View File
@@ -92,15 +92,15 @@ class MideaHumidifier(MideaEntity, HumidifierEntity):
"""Midea Humidifier is on."""
return cast("bool", self._device.get_attribute(attr="power"))
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Humidifier turn on."""
self._device.set_attribute(attr="power", value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Humidifier turn off."""
self._device.set_attribute(attr="power", value=False)
def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002
def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Humidifier update state."""
if not self.hass:
_LOGGER.warning(
@@ -109,7 +109,7 @@ class MideaHumidifier(MideaEntity, HumidifierEntity):
type(self),
)
return
self.schedule_update_ha_state()
self.schedule_update_if_running()
class MideaA1Humidifier(MideaHumidifier):
+4 -4
View File
@@ -153,7 +153,7 @@ class MideaLight(MideaEntity, LightEntity):
"""Midea Light effect."""
return cast("str", self._device.get_attribute(X13Attributes.effect))
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea Light turn on."""
if not self.is_on:
self._device.set_attribute(attr=X13Attributes.power, value=True)
@@ -168,11 +168,11 @@ class MideaLight(MideaEntity, LightEntity):
if key == ATTR_EFFECT:
self._device.set_attribute(attr=X13Attributes.effect, value=value)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Light turn off."""
self._device.set_attribute(attr=X13Attributes.power, value=False)
def update_state(self, status: Any) -> None: # noqa: ANN401,ARG002
def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Light update state."""
if not self.hass:
_LOGGER.warning(
@@ -181,4 +181,4 @@ class MideaLight(MideaEntity, LightEntity):
type(self),
)
return
self.schedule_update_ha_state()
self.schedule_update_if_running()
+4 -4
View File
@@ -38,17 +38,17 @@ class MideaLock(MideaEntity, LockEntity):
@property
def is_locked(self) -> bool:
"""Return true if state is locked."""
"""Whether the lock is locked."""
return cast("bool", self._device.get_attribute(self._entity_key))
def lock(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def lock(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Lock the lock."""
self._device.set_attribute(attr=self._entity_key, value=True)
def unlock(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def unlock(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Unlock the lock."""
self._device.set_attribute(attr=self._entity_key, value=False)
def open(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def open(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Open the lock."""
self.unlock()
+2 -2
View File
@@ -9,6 +9,6 @@
"iot_class": "local_push",
"issue_tracker": "https://github.com/wuwentao/midea_ac_lan/issues",
"loggers": ["midealocal"],
"requirements": ["midea-local==6.8.0"],
"version": "0.6.12"
"requirements": ["midea-local==6.11.0"],
"version": "0.7.0"
}
@@ -8,8 +8,12 @@ from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
Platform,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
@@ -50,6 +54,11 @@ from midealocal.devices.x26 import DeviceAttributes as X26Attributes
from midealocal.devices.x34 import DeviceAttributes as X34Attributes
from midealocal.devices.x40 import DeviceAttributes as X40Attributes
FRESH_AIR_EXHAUST = "fresh_air_exhaust"
FRESH_AIR_EXHAUST_MODE = "fresh_air_exhaust_mode"
FRESH_AIR_EXHAUST_POWER = "fresh_air_exhaust_power"
FRESH_AIR_EXHAUST_SPEED = "fresh_air_exhaust_speed"
"""
Entity Naming Rule:
@@ -384,6 +393,29 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = {
"name": "Fresh Air",
"icon": "mdi:fan",
},
ACAttributes.fresh_air_mode: {
"type": Platform.SELECT,
"required_attribute": ACAttributes.fresh_air_mode,
"translation_key": "fresh_air_mode",
"name": "Fresh Air Speed",
"icon": "mdi:fan-chevron-up",
"options": "fresh_air_fan_speeds",
},
FRESH_AIR_EXHAUST: {
"type": Platform.FAN,
"translation_key": "fresh_air_exhaust",
"name": "Fresh Air Exhaust",
"icon": "mdi:fan-reverse",
"required_attribute": FRESH_AIR_EXHAUST_POWER,
},
FRESH_AIR_EXHAUST_MODE: {
"type": Platform.SELECT,
"translation_key": "fresh_air_exhaust_mode",
"name": "Fresh Air Exhaust Speed",
"icon": "mdi:fan-chevron-down",
"options": "fresh_air_exhaust_fan_speeds",
"required_attribute": FRESH_AIR_EXHAUST_POWER,
},
ACAttributes.aux_heating: {
"type": Platform.SWITCH,
"translation_key": "aux_heating",
@@ -578,6 +610,125 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = {
"name": "Error Code",
"icon": "mdi:alert-box",
},
# group 1: compressor and refrigerant circuit
ACAttributes.compressor_frequency: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.compressor_frequency,
"translation_key": "compressor_frequency",
"name": "Compressor Frequency",
"device_class": SensorDeviceClass.FREQUENCY,
"unit": UnitOfFrequency.HERTZ,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.target_compressor_frequency: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.target_compressor_frequency,
"translation_key": "target_compressor_frequency",
"name": "Target Compressor Frequency",
"device_class": SensorDeviceClass.FREQUENCY,
"unit": UnitOfFrequency.HERTZ,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.compressor_current: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.compressor_current,
"translation_key": "compressor_current",
"name": "Compressor Current",
"device_class": SensorDeviceClass.CURRENT,
"unit": UnitOfElectricCurrent.AMPERE,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.compressor_voltage: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.compressor_voltage,
"translation_key": "compressor_voltage",
"name": "Compressor Voltage",
"device_class": SensorDeviceClass.VOLTAGE,
"unit": UnitOfElectricPotential.VOLT,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.indoor_coil_temperature: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.indoor_coil_temperature,
"translation_key": "indoor_coil_temperature",
"name": "Indoor Coil Temperature (T1)",
"device_class": SensorDeviceClass.TEMPERATURE,
"unit": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.evaporator_temperature: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.evaporator_temperature,
"translation_key": "evaporator_temperature",
"name": "Evaporator Temperature (T2)",
"device_class": SensorDeviceClass.TEMPERATURE,
"unit": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.condenser_temperature: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.condenser_temperature,
"translation_key": "condenser_temperature",
"name": "Condenser Temperature (T3)",
"device_class": SensorDeviceClass.TEMPERATURE,
"unit": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.outdoor_ambient_temperature: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.outdoor_ambient_temperature,
"translation_key": "outdoor_ambient_temperature",
"name": "Outdoor Ambient Temperature (T4)",
"device_class": SensorDeviceClass.TEMPERATURE,
"unit": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.discharge_pipe_temperature: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.discharge_pipe_temperature,
"translation_key": "discharge_pipe_temperature",
"name": "Discharge Pipe Temperature (TP)",
"device_class": SensorDeviceClass.TEMPERATURE,
"unit": UnitOfTemperature.CELSIUS,
"state_class": SensorStateClass.MEASUREMENT,
},
# group 2: indoor fan and condensate pump
ACAttributes.indoor_fan_speed: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.indoor_fan_speed,
"translation_key": "indoor_fan_speed",
"name": "Indoor Fan Speed",
"icon": "mdi:fan",
"unit": REVOLUTIONS_PER_MINUTE,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.target_indoor_fan_speed: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.target_indoor_fan_speed,
"translation_key": "target_indoor_fan_speed",
"name": "Target Indoor Fan Speed",
"icon": "mdi:fan-clock",
"unit": REVOLUTIONS_PER_MINUTE,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.water_pump_running: {
"type": Platform.BINARY_SENSOR,
"required_attribute": ACAttributes.water_pump_running,
"translation_key": "water_pump_running",
"name": "Water Pump Running",
"icon": "mdi:water-pump",
"device_class": BinarySensorDeviceClass.RUNNING,
},
# group 7: real time compressor power
ACAttributes.compressor_power: {
"type": Platform.SENSOR,
"required_attribute": ACAttributes.compressor_power,
"translation_key": "compressor_power",
"name": "Compressor Power",
"device_class": SensorDeviceClass.POWER,
"unit": UnitOfPower.WATT,
"state_class": SensorStateClass.MEASUREMENT,
},
ACAttributes.wind_lr_angle: {
"type": Platform.SELECT,
"translation_key": "wind_lr_angle",
@@ -592,6 +743,13 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = {
"options": "wind_ud_angles",
"icon": "mdi:pan-vertical",
},
ACAttributes.rate_select: {
"type": Platform.SELECT,
"translation_key": "rate_select",
"name": "Power Rate Limit",
"options": "rate_selects",
"icon": "mdi:lightning-bolt-circle",
},
ACAttributes.fan_speed: {
"type": Platform.NUMBER,
"translation_key": "fan_speed_percent",
@@ -2432,12 +2590,82 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = {
"name": "Storage",
"icon": "mdi:repeat-variant",
},
"start": {
"type": Platform.BUTTON,
"translation_key": "start",
"name": "Start",
"icon": "mdi:play",
"set_message": "e1_start",
"available_power_attribute": E1Attributes.power,
"models": ["7600024L"],
"default": True,
},
E1Attributes.mode: {
"type": Platform.SENSOR,
"translation_key": "mode",
"name": "Working Mode",
"icon": "mdi:dishwasher",
},
"mode_select": {
"type": Platform.SELECT,
"attribute": E1Attributes.mode,
"translation_key": "wash_mode",
"name": "Wash Mode",
"options_dict": "modes",
"options_codes_by_model": {
"7600024L": [13, 4, 8, 6, 2, 11, 10],
},
"set_message": "e1_work_mode",
"available_power_attribute": E1Attributes.power,
"icon": "mdi:dishwasher",
"default": True,
},
"estimated_energy_consumption": {
"type": Platform.SENSOR,
"translation_key": "estimated_energy_consumption",
"name": "Estimated Energy Consumption",
"icon": "mdi:lightning-bolt",
"device_class": SensorDeviceClass.ENERGY,
"unit": UnitOfEnergy.KILO_WATT_HOUR,
"state_class": SensorStateClass.TOTAL_INCREASING,
"estimate": {
"kind": "energy",
"values": {
"Germ": 0.765,
"ECO Wash": 0.99,
"Strong Wash": 1.28,
"Hour Wash": 0.91,
"Soak Wash": 0.02,
"Self Clean": 1.524,
"Fruit Wash": 1.625,
},
},
"models": ["7600024L"],
"default": True,
},
"estimated_water_consumption": {
"type": Platform.SENSOR,
"translation_key": "estimated_water_consumption",
"name": "Estimated Water Consumption",
"icon": "mdi:water",
"device_class": SensorDeviceClass.WATER,
"unit": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING,
"estimate": {
"kind": "water",
"values": {
"Germ": 9.9,
"ECO Wash": 10.4,
"Strong Wash": 13.9,
"Hour Wash": 10.4,
"Soak Wash": 3.4,
"Self Clean": 10.3,
"Fruit Wash": 13.3,
},
},
"models": ["7600024L"],
"default": True,
},
E1Attributes.error_code: {
"type": Platform.SENSOR,
"translation_key": "error_code",
@@ -2536,6 +2764,18 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = {
"name": "Whole Tank Heating",
"icon": "mdi:restore",
},
E2Attributes.sterilization: {
"type": Platform.SWITCH,
"translation_key": "sterilization",
"name": "Sterilization",
"icon": "mdi:bacteria",
},
E2Attributes.memory: {
"type": Platform.SWITCH,
"translation_key": "memory",
"name": "Memo U",
"icon": "mdi:brain",
},
},
},
0xE3: {
@@ -2951,6 +3191,176 @@ MIDEA_DEVICES: dict[int, dict[str, dict[str, Any] | str]] = {
"unit": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING,
},
# Soft water machine (water softener) entities
EDAttributes.soften: {
"type": Platform.SWITCH,
"translation_key": "soften",
"name": "Softening",
"icon": "mdi:water-outline",
},
EDAttributes.cl_sterilization: {
"type": Platform.SWITCH,
"translation_key": "cl_sterilization",
"name": "CL Sterilization",
"icon": "mdi:bacteria",
},
EDAttributes.leak_water_protection: {
"type": Platform.SWITCH,
"translation_key": "leak_water_protection",
"name": "Leak Water Protection",
"icon": "mdi:water-alert",
},
EDAttributes.water_way: {
"type": Platform.SWITCH,
"translation_key": "water_way",
"name": "Water Way",
"icon": "mdi:pipe",
},
EDAttributes.regeneration: {
"type": Platform.SWITCH,
"translation_key": "regeneration",
"name": "Regeneration",
"icon": "mdi:refresh",
},
EDAttributes.velocity: {
"type": Platform.SENSOR,
"translation_key": "velocity",
"name": "Velocity",
"icon": "mdi:speedometer",
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.soft_available: {
"type": Platform.SENSOR,
"translation_key": "soft_available",
"name": "Soft Water Available",
"icon": "mdi:water-check",
"unit": UnitOfVolume.LITERS,
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.left_salt: {
"type": Platform.SENSOR,
"translation_key": "left_salt",
"name": "Left Salt",
"icon": "mdi:shaker",
"unit": PERCENTAGE,
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.remaining_days: {
"type": Platform.SENSOR,
"translation_key": "remaining_days",
"name": "Remaining Days",
"icon": "mdi:calendar-clock",
"unit": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.water_hardness: {
"type": Platform.NUMBER,
"translation_key": "water_hardness",
"name": "Water Hardness",
"icon": "mdi:water",
"min": 0,
"max": 65535,
"step": 1,
},
EDAttributes.flushing_days: {
"type": Platform.NUMBER,
"translation_key": "flushing_days",
"name": "Flushing Days",
"icon": "mdi:water",
"unit": UnitOfTime.DAYS,
"min": 0,
"max": 99,
"step": 1,
},
EDAttributes.timing_regeneration_hour: {
"type": Platform.TIME,
"translation_key": "timing_regeneration",
"name": "Timing Regeneration",
"icon": "mdi:clock-outline",
},
EDAttributes.regeneration_left_seconds: {
"type": Platform.SENSOR,
"translation_key": "regeneration_left_seconds",
"name": "Regeneration Left Seconds",
"icon": "mdi:timer",
"unit": UnitOfTime.SECONDS,
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.use_days: {
"type": Platform.SENSOR,
"translation_key": "use_days",
"name": "Use Days",
"icon": "mdi:calendar",
"unit": UnitOfTime.DAYS,
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.salt_setting: {
"type": Platform.SENSOR,
"translation_key": "salt_setting",
"name": "Salt Setting",
"icon": "mdi:shaker-outline",
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.water_consumption_big: {
"type": Platform.SENSOR,
"translation_key": "water_consumption_big",
"name": "Water Consumption",
"icon": "mdi:water-pump",
"unit": UnitOfVolume.LITERS,
"state_class": SensorStateClass.TOTAL_INCREASING,
"suggested_display_precision": 2,
},
EDAttributes.water_consumption_average: {
"type": Platform.SENSOR,
"translation_key": "water_consumption_average",
"name": "Water Consumption Average",
"icon": "mdi:water",
"unit": UnitOfVolume.LITERS,
"state_class": SensorStateClass.MEASUREMENT,
},
EDAttributes.leak_water_protection_value: {
"type": Platform.NUMBER,
"translation_key": "leak_water_protection_value",
"name": "Leak Water Protection Value",
"icon": "mdi:water-alert-outline",
"unit": UnitOfVolume.LITERS,
"min": 0,
"max": 2550,
"step": 50,
},
EDAttributes.leak_water: {
"type": Platform.BINARY_SENSOR,
"translation_key": "leak_water",
"name": "Leak Water",
"icon": "mdi:water-alert",
"device_class": BinarySensorDeviceClass.PROBLEM,
},
EDAttributes.rsj_stand_by: {
"type": Platform.BINARY_SENSOR,
"translation_key": "rsj_stand_by",
"name": "Stand By",
"icon": "mdi:power-standby",
},
EDAttributes.error: {
"type": Platform.SENSOR,
"translation_key": "error",
"name": "Error",
"icon": "mdi:alert-circle",
"device_class": SensorDeviceClass.ENUM,
# Soft water machine (deviceKind=9/10, subtype 703) error codes
# Source: weex.js error dictionary "a"
"options": {
0: "no_error",
1: "e1_position_not_found",
2: "e2_photo_sensor_no_signal",
3: "e3_motor_not_running",
4: "e4_wrong_position",
225: "e1_motor_fault",
229: "e5_communication_fault",
230: "e6_salt_sensor_fault",
231: "e7_chlorine_sterilization_fault",
},
},
},
},
0xFA: {
+87 -21
View File
@@ -12,6 +12,7 @@ else:
from homeassistant.helpers.entity import ( # type: ignore[attr-defined]
DeviceInfo,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.entity import Entity
from midealocal.device import MideaDevice
@@ -27,14 +28,18 @@ class MideaEntity(Entity):
def __init__(self, device: MideaDevice, entity_key: str) -> None:
"""Initialize Midea base entity."""
self._device = device
self._device.register_update(self.update_state)
self._config = cast(
"dict",
MIDEA_DEVICES[self._device.device_type]["entities"],
)[entity_key]
self._entity_key = entity_key
self._unique_id = f"{DOMAIN}.{self._device.device_id}_{entity_key}"
self.entity_id = self._unique_id
# Build entity_id with the correct platform domain (sensor.*, switch.*, …)
# instead of the integration domain. Keeps the legacy "<device_id>_<key>"
# object_id so existing entity_ids are unchanged, while fixing the HA
# wrong-domain deprecation (breaks in HA 2027.5.0).
ha_domain = self._config["type"]
self.entity_id = f"{ha_domain}.{self._device.device_id}_{entity_key}"
self._device_name = self._device.name
# HA language setting:
@@ -43,7 +48,7 @@ class MideaEntity(Entity):
# Entity name translation based on hass.config.language
# add language in /config/configuration.yaml will disable web UI setting
# homeassistant:
# language: zh-Hans # noqa: ERA001
# language: zh-Hans # ruff:ignore[commented-out-code]
# Translating the name and attributes of entities:
# https://developers.home-assistant.io/blog/2023/03/27/entity_name_translations/#translating-entity-name
@@ -84,13 +89,13 @@ class MideaEntity(Entity):
@property
def device(self) -> MideaDevice:
"""Return device structure."""
"""Underlying Midea device instance."""
return self._device
@property
def device_info(self) -> DeviceInfo:
"""Return device info."""
return {
"""Device registry info for the entity."""
info: DeviceInfo = {
"manufacturer": "Midea",
"model": f"{MIDEA_DEVICES[self._device.device_type]['name']} "
f"{self._device.model}"
@@ -98,32 +103,100 @@ class MideaEntity(Entity):
"identifiers": {(DOMAIN, str(self._device.device_id))},
"name": self._device_name,
}
if self._device.mac:
info["connections"] = {
(CONNECTION_NETWORK_MAC, format_mac(self._device.mac)),
}
if self._device.serial_number:
info["serial_number"] = self._device.serial_number
return info
@property
def unique_id(self) -> str:
"""Return entity unique id."""
"""Unique id of the entity."""
return self._unique_id
@property
def should_poll(self) -> bool:
"""Return true is integration should poll."""
"""Whether the integration should poll for updates."""
return False
@property
def available(self) -> bool:
"""Return entity availability."""
"""Whether the entity is available."""
return bool(self._device.available)
@property
def icon(self) -> str:
"""Return entity icon."""
"""Icon for the entity."""
return cast("str", self._config.get("icon"))
async def async_added_to_hass(self) -> None:
"""Subscribe to device updates once the entity is added to HA.
Registering the callback here (rather than in ``__init__``) ensures an
entity that is constructed but never added to HA never receives updates,
which avoids the recurring "HASS is None" warnings.
"""
await super().async_added_to_hass()
self._device.register_update(self.update_state)
async def async_will_remove_from_hass(self) -> None:
"""Unsubscribe from device updates when the entity is removed from HA."""
await super().async_will_remove_from_hass()
self._device.unregister_update(self.update_state)
@callback
def update_state(self, status: Any) -> None: # noqa: ANN401
def schedule_update_if_running(self) -> None:
"""Schedule a state write unless HA is shutting down.
Shared by the base ``update_state`` and by the per-device-type
subclasses that override it (climate, fan, light, water_heater,
humidifier), so the shutdown guard lives in exactly one place and the
main control entities are protected too (issues #798 and #809).
The caller is responsible for the ``self.hass is None`` guard.
Raises:
RuntimeError: If ``schedule_update_ha_state()`` fails for a reason
other than the event loop being closed during shutdown.
"""
if self.hass.is_stopping:
_LOGGER.debug(
"MideaEntity update_state for %s [%s]: HASS is stopping",
self.name,
type(self),
)
return
# A device background thread can still deliver an update after the HA
# event loop has been closed during shutdown. Scheduling a state write
# then raises RuntimeError because the loop is already closed (issues
# #798 and #809). The is_stopping guard above covers the common case,
# but a race remains between that check and the schedule call, so also
# guard against the closed loop and swallow the residual RuntimeError.
if self.hass.loop.is_closed():
return
try:
self.schedule_update_ha_state()
except RuntimeError:
# Only swallow the shutdown race; re-raise any unrelated RuntimeError.
if not self.hass.loop.is_closed():
raise
_LOGGER.debug(
"Ignoring update for %s: event loop closed during shutdown",
self.name,
)
@callback
def update_state(self, status: Any) -> None: # ruff:ignore[any-type]
"""Update entity state."""
if not self.hass:
_LOGGER.warning(
# Defensive guard for the is_stopping access below. Since the update
# callback is now registered in async_added_to_hass (#869), hass is
# always set on the normal path, so this is debug (like is_stopping).
_LOGGER.debug(
"MideaEntity update_state for %s [%s] with status %s: HASS is None",
self.name,
type(self),
@@ -131,14 +204,7 @@ class MideaEntity(Entity):
)
return
if self.hass.is_stopping:
_LOGGER.debug(
"MideaEntity update_state for %s [%s] with status %s: HASS is stopping",
self.name,
type(self),
status,
)
if self._entity_key not in status and "available" not in status:
return
if self._entity_key in status or "available" in status:
self.schedule_update_ha_state()
self.schedule_update_if_running()
+5 -5
View File
@@ -46,7 +46,7 @@ class MideaNumber(MideaEntity, NumberEntity):
@property
def native_min_value(self) -> float:
"""Return minimum value."""
"""Minimum value allowed."""
return cast(
"float",
(
@@ -62,7 +62,7 @@ class MideaNumber(MideaEntity, NumberEntity):
@property
def native_max_value(self) -> float:
"""Return maximum value."""
"""Maximum value allowed."""
return cast(
"float",
(
@@ -78,7 +78,7 @@ class MideaNumber(MideaEntity, NumberEntity):
@property
def native_step(self) -> float:
"""Return step value."""
"""Step value between allowed values."""
return cast(
"float",
(
@@ -94,9 +94,9 @@ class MideaNumber(MideaEntity, NumberEntity):
@property
def native_value(self) -> float:
"""Return value."""
"""Native value of the entity."""
return cast("float", self._device.get_attribute(self._entity_key))
def set_native_value(self, value: Any) -> None: # noqa: ANN401
def set_native_value(self, value: Any) -> None: # ruff:ignore[any-type]
"""Set value."""
self._device.set_attribute(self._entity_key, value)
+92 -11
View File
@@ -1,18 +1,21 @@
"""Select for Midea Lan."""
from typing import cast
from typing import TYPE_CHECKING, Any, cast
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_SWITCHES, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from midealocal.device import MideaDevice
from .const import DEVICES, DOMAIN
from .const import DEVICES, DOMAIN, supports_model
from .midea_devices import MIDEA_DEVICES
from .midea_entity import MideaEntity
if TYPE_CHECKING:
from midealocal.devices.e1 import MideaE1Device
async def async_setup_entry(
hass: HomeAssistant,
@@ -28,9 +31,20 @@ async def async_setup_entry(
"dict",
MIDEA_DEVICES[device.device_type]["entities"],
).items():
if config["type"] == Platform.SELECT and entity_key in extra_switches:
dev = MideaSelect(device, entity_key)
selects.append(dev)
if (
config["type"] != Platform.SELECT
or not supports_model(device.model, config)
or (not config.get("default") and entity_key not in extra_switches)
):
continue
required_attribute = config.get("required_attribute")
if (
required_attribute is not None
and required_attribute not in device.attributes
):
continue
dev = MideaSelect(device, entity_key)
selects.append(dev)
async_add_entities(selects)
@@ -40,18 +54,85 @@ class MideaSelect(MideaEntity, SelectEntity):
def __init__(self, device: MideaDevice, entity_key: str) -> None:
"""Midea select init."""
super().__init__(device, entity_key)
self._attribute_key = self._config.get("attribute", entity_key)
self._options_name = self._config.get("options")
self._options_dict_name = self._config.get("options_dict")
@property
def options(self) -> list[str]:
"""Return entity options."""
"""Available options for the entity."""
if self._options_dict_name:
options = self._get_options_dict()
codes_by_model = self._config.get("options_codes_by_model", {})
codes = codes_by_model.get(str(self._device.model)) or self._config.get(
"options_codes",
)
if codes:
return [options[code] for code in codes if code in options]
return list(options.values())
return cast("list", getattr(self._device, self._options_name))
@property
def current_option(self) -> str:
"""Return entity current option."""
return cast("str", self._device.get_attribute(self._entity_key))
def current_option(self) -> str | None:
"""Currently selected option."""
option = cast("str | None", self._device.get_attribute(self._attribute_key))
return option if option in self.options else None
@property
def available(self) -> bool:
"""Whether the entity is available."""
if not super().available:
return False
power_attribute = self._config.get("available_power_attribute")
return not power_attribute or bool(self._device.get_attribute(power_attribute))
def select_option(self, option: str) -> None:
"""Select entity option."""
self._device.set_attribute(self._entity_key, option)
if self._config.get("set_message") == "e1_work_mode":
self._select_e1_work_mode(option)
return
self._device.set_attribute(self._attribute_key, option)
def _get_options_dict(self) -> dict[int, str]:
"""Return option dict from the backing midea-local device.
Returns
-------
Option labels keyed by the raw mode code.
"""
return cast("dict[int, str]", getattr(self._device, self._options_dict_name))
def _select_e1_work_mode(self, option: str) -> None:
"""Set dishwasher work mode via midea-local's public E1 API.
Raises
------
ValueError
If the requested option is not a supported work mode.
"""
mode = self._get_dict_key_by_value(self._get_options_dict(), option)
if mode is None:
raise ValueError(f"Unsupported dishwasher mode: {option}")
cast("MideaE1Device", self._device).set_work_mode(mode)
@callback
def update_state(self, status: Any) -> None: # ruff:ignore[any-type]
"""Update entity state."""
super().update_state(status)
power_attribute = self._config.get("available_power_attribute")
if (
power_attribute
and self.hass
and not self.hass.is_stopping
and (self._attribute_key in status or power_attribute in status)
):
self.schedule_update_ha_state()
@staticmethod
def _get_dict_key_by_value(source: dict[int, str], value: str) -> int | None:
for key, item in source.items():
if item == value:
return key
return None
+136 -10
View File
@@ -11,9 +11,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_SENSORS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType
from midealocal.device import MideaDevice
from .const import DEVICES, DOMAIN
from .const import DEVICES, DOMAIN, supports_model
from .midea_devices import MIDEA_DEVICES
from .midea_entity import MideaEntity
@@ -32,9 +34,24 @@ async def async_setup_entry(
"dict",
MIDEA_DEVICES[device.device_type]["entities"],
).items():
if config["type"] == Platform.SENSOR and entity_key in extra_sensors:
sensor = MideaSensor(device, entity_key)
sensors.append(sensor)
if (
config["type"] != Platform.SENSOR
or not supports_model(device.model, config)
or (not config.get("default") and entity_key not in extra_sensors)
):
continue
required_attribute = config.get("required_attribute")
if (
required_attribute is not None
and required_attribute not in device.attributes
):
continue
sensor = (
MideaEstimatedUsageSensor(device, entity_key)
if config.get("estimate")
else MideaSensor(device, entity_key)
)
sensors.append(sensor)
async_add_entities(sensors)
@@ -43,25 +60,134 @@ class MideaSensor(MideaEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Return entity value."""
return cast("StateType", self._device.get_attribute(self._entity_key))
"""Native value of the sensor."""
value = self._device.get_attribute(self._entity_key)
# If an options mapping exists, translate the raw value to its enum key.
options = self._config.get("options")
if options is not None and isinstance(value, int):
# For an enum sensor an unmapped value is not a valid state, so
# report it as unknown (None) instead of leaking the raw int.
return cast("StateType", options.get(value))
return cast("StateType", value)
@property
def device_class(self) -> SensorDeviceClass:
"""Return device class."""
"""Device class of the sensor."""
return cast("SensorDeviceClass", self._config.get("device_class"))
@property
def state_class(self) -> SensorStateClass | None:
"""Return state state."""
"""State class of the sensor."""
return cast("SensorStateClass | None", self._config.get("state_class"))
@property
def options(self) -> list[str] | None:
"""List of possible states for an enum sensor."""
if self.device_class != SensorDeviceClass.ENUM:
return None
options = self._config.get("options")
return list(options.values()) if options else None
@property
def native_unit_of_measurement(self) -> str | None:
"""Return unit of measurement."""
"""Unit of measurement for the sensor."""
return cast("str | None", self._config.get("unit"))
@property
def suggested_display_precision(self) -> int | None:
"""Suggested number of decimal digits for display."""
return cast("int | None", self._config.get("suggested_display_precision"))
@property
def capability_attributes(self) -> dict[str, Any] | None:
"""Return capabilities."""
"""Capability attributes of the sensor."""
if self.options is not None:
return {"options": self.options}
return {"state_class": self.state_class} if self.state_class else {}
class MideaEstimatedUsageSensor(MideaSensor, RestoreEntity):
"""Represent estimated dishwasher usage accumulated per run.
The dishwasher does not report actual energy/water usage, so this sensor
accumulates a fixed per-mode estimate (from the product manual) once each
time a wash run *completes*. Completion is detected by the device
``progress`` attribute reaching ``"Complete"``; a cancelled or errored run
never reaches that state and is therefore not counted.
"""
_COMPLETE_PROGRESS = "Complete"
_RUNNING_STATUS = "Running"
def __init__(self, device: MideaDevice, entity_key: str) -> None:
"""Initialize estimated usage sensor."""
super().__init__(device, entity_key)
self._native_value: float = 0.0
self._last_progress: str | None = None
self._running_mode: str | None = None
async def async_added_to_hass(self) -> None:
"""Register for device updates and restore the accumulated value."""
await super().async_added_to_hass()
if last_state := await self.async_get_last_state():
try:
self._native_value = float(last_state.state)
except (TypeError, ValueError):
self._native_value = 0.0
self._last_progress = cast(
"str | None",
self._device.get_attribute("progress"),
)
if self._device.get_attribute("status") == self._RUNNING_STATUS:
self._running_mode = cast("str | None", self._device.get_attribute("mode"))
@property
def native_value(self) -> StateType:
"""Accumulated estimated usage."""
return round(self._native_value, 3)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Estimate metadata for the sensor."""
estimate = cast("dict[str, Any]", self._config["estimate"])
return {
"estimate_source": "fixed_per_wash_mode",
"known_modes": list(cast("dict[str, float]", estimate["values"]).keys()),
"last_progress": self._last_progress,
"running_mode": self._running_mode,
}
def update_state(self, status: Any) -> None: # ruff:ignore[any-type]
"""Accumulate the estimate once when a dishwasher run completes."""
current_status = self._device.get_attribute("status")
current_progress = cast(
"str | None",
self._device.get_attribute("progress"),
)
current_mode = cast("str | None", self._device.get_attribute("mode"))
# Remember the mode selected while the machine is actually running so we
# can still attribute usage after it stops reporting the mode on finish.
if current_status == self._RUNNING_STATUS:
self._running_mode = current_mode
# Count once, on the edge into the "Complete" progress state. A cancel or
# error transition never reaches "Complete", so it is not counted.
if (
current_progress == self._COMPLETE_PROGRESS
and self._last_progress != self._COMPLETE_PROGRESS
):
mode = self._running_mode or current_mode
values = cast("dict[str, float]", self._config["estimate"]["values"])
if mode in values:
self._native_value += values[mode]
self._running_mode = None
self._last_progress = current_progress
super().update_state(status)
if (
self.hass
and not self.hass.is_stopping
and ("progress" in status or "status" in status or "mode" in status)
):
self.schedule_update_ha_state()
+3 -3
View File
@@ -38,13 +38,13 @@ class MideaSwitch(MideaEntity, ToggleEntity):
@property
def is_on(self) -> bool:
"""Return true if switch is on."""
"""Whether the switch is on."""
return cast("bool", self._device.get_attribute(self._entity_key))
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Turn on switch."""
self._device.set_attribute(attr=self._entity_key, value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Turn off switch."""
self._device.set_attribute(attr=self._entity_key, value=False)
@@ -93,6 +93,9 @@
"cooking": {
"name": "Cooking"
},
"error_code": {
"name": "Fehlercode"
},
"filter_change_reminder": {
"name": "Filter Change Reminder"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "Keep Warm"
},
"leak_water": {
"name": "Leckwasser"
},
"lid_status": {
"name": "Lid Status"
},
@@ -138,6 +144,21 @@
"middle_compartment_preheating": {
"name": "Middle Compartment Preheating"
},
"top_elec_heat": {
"name": "Elektroheizung oben"
},
"bottom_elec_heat": {
"name": "Elektroheizung unten"
},
"mute_effect": {
"name": "Stummschaltung aktiv"
},
"mute_status": {
"name": "Stummschaltungsstatus"
},
"multi_terminal": {
"name": "Multi-Terminal"
},
"oilcup_full": {
"name": "Oil-cup Full"
},
@@ -153,12 +174,33 @@
"rinse_aid": {
"name": "Rinse Aid Shortage"
},
"rsj_stand_by": {
"name": "Standby"
},
"salt": {
"name": "Salt Shortage"
},
"seat_status": {
"name": "Seat Status"
},
"smart_grid": {
"name": "Smart Grid"
},
"eco": {
"name": "ECO"
},
"maintenance_reminder": {
"name": "Wartungserinnerung"
},
"maintain_warn": {
"name": "Wartungswarnung"
},
"order1_effect": {
"name": "Zeitplan 1 aktiv"
},
"order2_effect": {
"name": "Zeitplan 2 aktiv"
},
"status_dhw": {
"name": "DHW status"
},
@@ -206,6 +248,20 @@
},
"zone2_water_temp_mode": {
"name": "Zone2 Water-temperature Mode"
},
"microcrystal_fresh": {
"name": "Mikrokristall-Frische"
},
"electronic_smell": {
"name": "Desodorieren und Sterilisieren"
},
"water_pump_running": {
"name": "Wasserpumpe läuft"
}
},
"button": {
"start": {
"name": "Start"
}
},
"climate": {
@@ -214,11 +270,24 @@
},
"climate_zone2": {
"name": "Zone2 Thermostat"
},
"climate_key": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Leise",
"full": "Voll"
}
}
}
}
},
"fan": {
"fresh_air": {
"name": "Fresh Air"
},
"fresh_air_exhaust": {
"name": "Frischluft-Abluft"
}
},
"lock": {
@@ -238,6 +307,21 @@
},
"water_temp_level": {
"name": "Water Temperature Level"
},
"fan_speed_percent": {
"name": "Lüftergeschwindigkeit (Prozent)"
},
"vacation_days": {
"name": "Urlaubstage"
},
"leak_water_protection_value": {
"name": "Leckschutzwert"
},
"flushing_days": {
"name": "Regenerationstage"
},
"water_hardness": {
"name": "Wasserhärte"
}
},
"select": {
@@ -250,6 +334,25 @@
"fan_speed": {
"name": "Fan Speed"
},
"fresh_air_exhaust_mode": {
"name": "Frischluft-Abluftgeschwindigkeit",
"state": {
"off": "Aus",
"silent": "Niedrig",
"high": "Hoch",
"full": "Voll"
}
},
"fresh_air_mode": {
"name": "Frischluftgeschwindigkeit",
"state": {
"off": "Aus",
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch",
"full": "Voll"
}
},
"mode": {
"name": "Mode"
},
@@ -259,6 +362,9 @@
"oscillation_mode": {
"name": "Oscillation Mode"
},
"rate_select": {
"name": "Power Rate Limit"
},
"screen_display": {
"name": "Screen Display"
},
@@ -268,8 +374,38 @@
"tilting_angle": {
"name": "Tilting Angle"
},
"wash_mode": {
"name": "Waschmodus"
},
"water_level_set": {
"name": "Water Level Setting"
},
"wind_lr_angle": {
"name": "Luftstrom horizontal",
"state": {
"off": "Aus",
"left": "Links",
"left-mid": "Links-Mitte",
"middle": "Mitte",
"right-mid": "Rechts-Mitte",
"right": "Rechts"
}
},
"wind_ud_angle": {
"name": "Luftstrom vertikal",
"state": {
"off": "Aus",
"up": "Oben",
"up-mid": "Oben-Mitte",
"middle": "Mitte",
"down-mid": "Unten-Mitte",
"down": "Unten"
}
}
},
"time": {
"timing_regeneration": {
"name": "Regenerationszeit"
}
},
"sensor": {
@@ -309,15 +445,71 @@
"dehydration_time": {
"name": "dehydration time"
},
"dehydration_time_value": {
"name": "Schleuderzeitwert"
},
"temperature": {
"name": "Temperatur"
},
"wash_time_value": {
"name": "Waschzeitwert"
},
"stains": {
"name": "Flecken"
},
"dirty_degree": {
"name": "Verschmutzungsgrad"
},
"detergent": {
"name": "detergent"
},
"intensity": {
"name": "Intensität"
},
"dryness_level": {
"name": "Trocknungsgrad"
},
"dry_temperature": {
"name": "Trocknungstemperatur"
},
"door_warn": {
"name": "Türwarnung"
},
"ai_switch": {
"name": "KI-Schalter"
},
"material": {
"name": "Material"
},
"water_box": {
"name": "Wasserbehälter"
},
"energy_consumption": {
"name": "Energy Consumption"
},
"error": {
"name": "Fehler",
"state": {
"no_error": "Kein Fehler",
"e1_position_not_found": "E1 Position nicht gefunden",
"e2_photo_sensor_no_signal": "E2 Fotosensor kein Signal",
"e3_motor_not_running": "E3 Motor läuft nicht",
"e4_wrong_position": "E4 Falsche Position",
"e1_motor_fault": "E1 Motorfehler",
"e5_communication_fault": "E5 Kommunikationsfehler",
"e6_salt_sensor_fault": "E6 Salzsensorfehler",
"e7_chlorine_sterilization_fault": "E7 Chlorsterilisationsfehler"
}
},
"error_code": {
"name": "Error Code"
},
"estimated_energy_consumption": {
"name": "Geschätzter Energieverbrauch"
},
"estimated_water_consumption": {
"name": "Geschätzter Wasserverbrauch"
},
"fan_level": {
"name": "Fan level"
},
@@ -381,6 +573,9 @@
"keep_warm_time": {
"name": "Keep Warm Time"
},
"left_salt": {
"name": "Verbleibendes Salz"
},
"middle_compartment_remaining": {
"name": "Middle Compartment Remaining"
},
@@ -417,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "Refrigerator Setting Temperature"
},
"regeneration_left_seconds": {
"name": "Regeneration verbleibende Sekunden"
},
"remaining_days": {
"name": "Verbleibende Tage"
},
"right_flex_zone_actual_temp": {
"name": "Right Flex Zone Actual Temperature"
},
@@ -429,12 +630,18 @@
"rinse_level": {
"name": "rinse level"
},
"salt_setting": {
"name": "Salzeinstellung"
},
"seat_temperature": {
"name": "Seat Temperature"
},
"soak_time": {
"name": "soak time"
},
"soft_available": {
"name": "Verfügbares Weichwasser"
},
"softener": {
"name": "softener"
},
@@ -492,14 +699,110 @@
"water_consumption": {
"name": "Wasserverbrauch"
},
"water_consumption_average": {
"name": "Durchschnittlicher Wasserverbrauch"
},
"water_consumption_big": {
"name": "Wasserverbrauch"
},
"water_temperature": {
"name": "Water Temperature"
},
"water_level": {
"name": "Water Level"
},
"wind": {
"name": "Wind"
},
"typeinfo": {
"name": "Typinformation"
},
"use_days": {
"name": "Nutzungstage"
},
"elec_heat": {
"name": "Elektroheizung"
},
"water_pump": {
"name": "Wasserpumpe"
},
"four_way": {
"name": "Vierwegeventil"
},
"back_water": {
"name": "Rücklaufwasser"
},
"sterilize": {
"name": "Sterilisieren"
},
"disinfection_temperature": {
"name": "Desinfektionstemperatur"
},
"max_temperature": {
"name": "Maximale Zieltemperatur"
},
"vacation_start_year": {
"name": "Urlaubsbeginn Jahr"
},
"vacation_start_month": {
"name": "Urlaubsbeginn Monat"
},
"vacation_start_day": {
"name": "Urlaubsbeginn Tag"
},
"auto_sterilize_week": {
"name": "Auto-Sterilisieren Woche"
},
"auto_sterilize_hour": {
"name": "Auto-Sterilisieren Stunde"
},
"auto_sterilize_minute": {
"name": "Auto-Sterilisieren Minute"
},
"working_time": {
"name": "Working Time"
},
"humidity": {
"name": "Luftfeuchtigkeit"
},
"variable_mode": {
"name": "Variabler Modus"
},
"compressor_frequency": {
"name": "Kompressorfrequenz"
},
"target_compressor_frequency": {
"name": "Ziel-Kompressorfrequenz"
},
"compressor_current": {
"name": "Kompressorstrom"
},
"compressor_voltage": {
"name": "Kompressorspannung"
},
"compressor_power": {
"name": "Kompressorleistung"
},
"indoor_coil_temperature": {
"name": "Innenraum-Wärmetauschertemperatur (T1)"
},
"evaporator_temperature": {
"name": "Verdampfertemperatur (T2)"
},
"outdoor_ambient_temperature": {
"name": "Außenumgebungstemperatur (T4)"
},
"discharge_pipe_temperature": {
"name": "Druckleitungstemperatur (TP)"
},
"indoor_fan_speed": {
"name": "Innenlüftergeschwindigkeit"
},
"target_indoor_fan_speed": {
"name": "Ziel-Innenlüftergeschwindigkeit"
},
"velocity": {
"name": "Durchflussgeschwindigkeit"
}
},
"switch": {
@@ -515,6 +818,12 @@
"breezeless": {
"name": "Breezeless"
},
"child_lock": {
"name": "Kindersicherung"
},
"cl_sterilization": {
"name": "CL-Sterilisation"
},
"comfort_mode": {
"name": "Comfort Mode"
},
@@ -524,12 +833,24 @@
"disinfect": {
"name": "Disinfect"
},
"vacation_mode": {
"name": "Urlaubsmodus"
},
"dry": {
"name": "Dry"
},
"cold_water_single": {
"name": "Kaltwasser einzeln"
},
"cold_water_dot": {
"name": "Kaltwasser Punkt"
},
"eco_mode": {
"name": "ECO Mode"
},
"memory": {
"name": "Memo U"
},
"fast_dhw": {
"name": "Fast DHW"
},
@@ -545,6 +866,9 @@
"indirect_wind": {
"name": "Indirect Wind"
},
"leak_water_protection": {
"name": "Leckschutz"
},
"light": {
"name": "Licht"
},
@@ -575,9 +899,15 @@
"prompt_tone": {
"name": "Prompt Tone"
},
"regeneration": {
"name": "Regeneration"
},
"water_pump": {
"name": "Wasserpumpe"
},
"water_way": {
"name": "Wasserweg"
},
"screen_display": {
"name": "Screen Display"
},
@@ -593,6 +923,9 @@
"sleep_mode": {
"name": "Sleep Mode"
},
"out_silent": {
"name": "Außengerät Leisemodus"
},
"smart_eye": {
"name": "Smart Eye"
},
@@ -602,12 +935,18 @@
"smelly_sensor": {
"name": "Geruchssensor"
},
"soften": {
"name": "Enthärtung"
},
"standby": {
"name": "Standby"
},
"start": {
"name": "Start"
},
"sterilization": {
"name": "Sterilisation"
},
"storage": {
"name": "Storage"
},
@@ -93,6 +93,9 @@
"cooking": {
"name": "Cooking"
},
"error_code": {
"name": "Error Code"
},
"filter_change_reminder": {
"name": "Filter Change Reminder"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "Keep Warm"
},
"leak_water": {
"name": "Leak Water"
},
"lid_status": {
"name": "Lid Status"
},
@@ -168,6 +174,9 @@
"rinse_aid": {
"name": "Rinse Aid Shortage"
},
"rsj_stand_by": {
"name": "Stand By"
},
"salt": {
"name": "Salt Shortage"
},
@@ -237,11 +246,22 @@
"zone2_room_temp_mode": {
"name": "Zone2 Room-temperature Mode"
},
"zone2_water_temp_mode": {
"name": "Zone2 Water-temperature Mode"
},
"microcrystal_fresh": {
"name": "Microcrystal Fresh"
},
"electronic_smell": {
"name": "Deodorizing sterilizing"
},
"water_pump_running": {
"name": "Water Pump Running"
}
},
"button": {
"start": {
"name": "Start"
}
},
"climate": {
@@ -265,6 +285,9 @@
"fan": {
"fresh_air": {
"name": "Fresh Air"
},
"fresh_air_exhaust": {
"name": "Fresh Air Exhaust"
}
},
"lock": {
@@ -290,6 +313,15 @@
},
"vacation_days": {
"name": "Vacation Days"
},
"leak_water_protection_value": {
"name": "Leak Water Protection Value"
},
"flushing_days": {
"name": "Regeneration Days"
},
"water_hardness": {
"name": "Water Hardness"
}
},
"select": {
@@ -302,6 +334,25 @@
"fan_speed": {
"name": "Fan Speed"
},
"fresh_air_exhaust_mode": {
"name": "Fresh Air Exhaust Speed",
"state": {
"off": "Off",
"silent": "Low",
"high": "High",
"full": "Full"
}
},
"fresh_air_mode": {
"name": "Fresh Air Speed",
"state": {
"off": "Off",
"low": "Low",
"medium": "Medium",
"high": "High",
"full": "Full"
}
},
"mode": {
"name": "Mode"
},
@@ -311,6 +362,9 @@
"oscillation_mode": {
"name": "Oscillation Mode"
},
"rate_select": {
"name": "Power Rate Limit"
},
"screen_display": {
"name": "Screen Display"
},
@@ -320,6 +374,9 @@
"tilting_angle": {
"name": "Tilting Angle"
},
"wash_mode": {
"name": "Wash Mode"
},
"water_level_set": {
"name": "Water Level Setting"
},
@@ -346,6 +403,11 @@
}
}
},
"time": {
"timing_regeneration": {
"name": "Timing Regeneration"
}
},
"sensor": {
"bathing_leaving_temperature": {
"name": "Bathing Leaving Water Temperature"
@@ -386,6 +448,9 @@
"dehydration_time_value": {
"name": "Dehydration time value"
},
"temperature": {
"name": "Temperature"
},
"wash_time_value": {
"name": "Wash time value"
},
@@ -422,9 +487,29 @@
"energy_consumption": {
"name": "Energy Consumption"
},
"error": {
"name": "Error",
"state": {
"no_error": "No Error",
"e1_position_not_found": "E1 Position not found",
"e2_photo_sensor_no_signal": "E2 Photo sensor no signal",
"e3_motor_not_running": "E3 Motor not running",
"e4_wrong_position": "E4 Wrong position",
"e1_motor_fault": "E1 Motor fault",
"e5_communication_fault": "E5 Communication fault",
"e6_salt_sensor_fault": "E6 Salt sensor fault",
"e7_chlorine_sterilization_fault": "E7 Chlorine sterilization fault"
}
},
"error_code": {
"name": "Error Code"
},
"estimated_energy_consumption": {
"name": "Estimated Energy Consumption"
},
"estimated_water_consumption": {
"name": "Estimated Water Consumption"
},
"fan_level": {
"name": "Fan level"
},
@@ -488,6 +573,9 @@
"keep_warm_time": {
"name": "Keep Warm Time"
},
"left_salt": {
"name": "Left Salt"
},
"middle_compartment_remaining": {
"name": "Middle Compartment Remaining"
},
@@ -524,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "Refrigerator Setting Temperature"
},
"regeneration_left_seconds": {
"name": "Regeneration Left Seconds"
},
"remaining_days": {
"name": "Remaining Days"
},
"right_flex_zone_actual_temp": {
"name": "Right Flex Zone Actual Temperature"
},
@@ -536,12 +630,18 @@
"rinse_level": {
"name": "Rinse level"
},
"salt_setting": {
"name": "Salt Setting"
},
"seat_temperature": {
"name": "Seat Temperature"
},
"soak_time": {
"name": "Soak time"
},
"soft_available": {
"name": "Soft Water Available"
},
"softener": {
"name": "Softener"
},
@@ -587,6 +687,9 @@
"tvoc": {
"name": "TVOC"
},
"wash_level": {
"name": "Rinse count"
},
"wash_strength": {
"name": "Wash strength"
},
@@ -596,6 +699,12 @@
"water_consumption": {
"name": "Water Consumption"
},
"water_consumption_average": {
"name": "Water Consumption Average"
},
"water_consumption_big": {
"name": "Water Consumption"
},
"water_temperature": {
"name": "Water Temperature"
},
@@ -608,6 +717,9 @@
"typeinfo": {
"name": "Type Info"
},
"use_days": {
"name": "Use Days"
},
"elec_heat": {
"name": "Electric Heat"
},
@@ -655,6 +767,42 @@
},
"variable_mode": {
"name": "Variable Mode"
},
"compressor_frequency": {
"name": "Compressor Frequency"
},
"target_compressor_frequency": {
"name": "Target Compressor Frequency"
},
"compressor_current": {
"name": "Compressor Current"
},
"compressor_voltage": {
"name": "Compressor Voltage"
},
"compressor_power": {
"name": "Compressor Power"
},
"indoor_coil_temperature": {
"name": "Indoor Coil Temperature (T1)"
},
"evaporator_temperature": {
"name": "Evaporator Temperature (T2)"
},
"outdoor_ambient_temperature": {
"name": "Outdoor Ambient Temperature (T4)"
},
"discharge_pipe_temperature": {
"name": "Discharge Pipe Temperature (TP)"
},
"indoor_fan_speed": {
"name": "Indoor Fan Speed"
},
"target_indoor_fan_speed": {
"name": "Target Indoor Fan Speed"
},
"velocity": {
"name": "Velocity"
}
},
"switch": {
@@ -670,6 +818,12 @@
"breezeless": {
"name": "Breezeless"
},
"child_lock": {
"name": "Child Lock"
},
"cl_sterilization": {
"name": "CL Sterilization"
},
"comfort_mode": {
"name": "Comfort Mode"
},
@@ -694,6 +848,9 @@
"eco_mode": {
"name": "ECO Mode"
},
"memory": {
"name": "Memo U"
},
"fast_dhw": {
"name": "Fast DHW"
},
@@ -709,6 +866,9 @@
"indirect_wind": {
"name": "Indirect Wind"
},
"leak_water_protection": {
"name": "Leak Water Protection"
},
"light": {
"name": "Light"
},
@@ -739,9 +899,15 @@
"prompt_tone": {
"name": "Prompt Tone"
},
"regeneration": {
"name": "Regeneration"
},
"water_pump": {
"name": "Water Pump"
},
"water_way": {
"name": "Water Way"
},
"screen_display": {
"name": "Screen Display"
},
@@ -769,12 +935,18 @@
"smelly_sensor": {
"name": "Smelly Sensor"
},
"soften": {
"name": "Softening"
},
"standby": {
"name": "Standby"
},
"start": {
"name": "Start"
},
"sterilization": {
"name": "Sterilization"
},
"storage": {
"name": "Storage"
},
@@ -93,6 +93,9 @@
"cooking": {
"name": "Cocinando"
},
"error_code": {
"name": "Código de error"
},
"filter_change_reminder": {
"name": "Recordatorio de cambio de filtro"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "Mantener caliente"
},
"leak_water": {
"name": "Fuga de agua"
},
"lid_status": {
"name": "Estado de la tapa"
},
@@ -138,6 +144,21 @@
"middle_compartment_preheating": {
"name": "Precalentamiento del compartimento medio"
},
"top_elec_heat": {
"name": "Calefacción eléctrica superior"
},
"bottom_elec_heat": {
"name": "Calefacción eléctrica inferior"
},
"mute_effect": {
"name": "Efecto de silencio"
},
"mute_status": {
"name": "Estado de silencio"
},
"multi_terminal": {
"name": "Terminal múltiple"
},
"oilcup_full": {
"name": "Depósito de aceite lleno"
},
@@ -153,12 +174,33 @@
"rinse_aid": {
"name": "Escasez de abrillantador"
},
"rsj_stand_by": {
"name": "En espera"
},
"salt": {
"name": "Escasez de sal"
},
"seat_status": {
"name": "Estado del asiento"
},
"smart_grid": {
"name": "Red inteligente"
},
"eco": {
"name": "ECO"
},
"maintenance_reminder": {
"name": "Recordatorio de mantenimiento"
},
"maintain_warn": {
"name": "Advertencia de mantenimiento"
},
"order1_effect": {
"name": "Programación 1 activa"
},
"order2_effect": {
"name": "Programación 2 activa"
},
"status_dhw": {
"name": "Estado ACS"
},
@@ -204,11 +246,22 @@
"zone2_room_temp_mode": {
"name": "Zona 2 Modo temperatura ambiente"
},
"zone2_water_temp_mode": {
"name": "Zona 2 Modo temperatura del agua"
},
"microcrystal_fresh": {
"name": "Frescura microcristalina"
},
"electronic_smell": {
"name": "Esterilización desodorizante"
},
"water_pump_running": {
"name": "Bomba de agua en funcionamiento"
}
},
"button": {
"start": {
"name": "Iniciar"
}
},
"climate": {
@@ -232,6 +285,9 @@
"fan": {
"fresh_air": {
"name": "Aire fresco"
},
"fresh_air_exhaust": {
"name": "Extracción de aire fresco"
}
},
"lock": {
@@ -254,6 +310,18 @@
},
"fan_speed_percent": {
"name": "Porcentaje de velocidad del ventilador"
},
"vacation_days": {
"name": "Días de vacaciones"
},
"leak_water_protection_value": {
"name": "Valor de protección contra fugas"
},
"flushing_days": {
"name": "Días de regeneración"
},
"water_hardness": {
"name": "Dureza del agua"
}
},
"select": {
@@ -266,6 +334,25 @@
"fan_speed": {
"name": "Velocidad del ventilador"
},
"fresh_air_exhaust_mode": {
"name": "Velocidad de extracción de aire fresco",
"state": {
"off": "Apagado",
"silent": "Baja",
"high": "Alta",
"full": "Máximo"
}
},
"fresh_air_mode": {
"name": "Velocidad de aire fresco",
"state": {
"off": "Apagado",
"low": "Baja",
"medium": "Media",
"high": "Alta",
"full": "Máximo"
}
},
"mode": {
"name": "Modo"
},
@@ -275,6 +362,9 @@
"oscillation_mode": {
"name": "Modo de oscilación"
},
"rate_select": {
"name": "Límite de tasa de potencia"
},
"screen_display": {
"name": "Visualización de pantalla"
},
@@ -284,6 +374,9 @@
"tilting_angle": {
"name": "Ángulo de inclinación"
},
"wash_mode": {
"name": "Modo de lavado"
},
"water_level_set": {
"name": "Ajuste de nivel de agua"
},
@@ -310,6 +403,11 @@
}
}
},
"time": {
"timing_regeneration": {
"name": "Regeneración programada"
}
},
"sensor": {
"bathing_leaving_temperature": {
"name": "Temperatura de salida del agua de baño"
@@ -350,6 +448,9 @@
"dehydration_time_value": {
"name": "Valor del tiempo de centrifugado"
},
"temperature": {
"name": "Temperatura"
},
"wash_time_value": {
"name": "Valor del tiempo de lavado"
},
@@ -386,9 +487,29 @@
"energy_consumption": {
"name": "Consumo de energía"
},
"error": {
"name": "Error",
"state": {
"no_error": "Sin error",
"e1_position_not_found": "E1 Posición no encontrada",
"e2_photo_sensor_no_signal": "E2 Sensor foto sin señal",
"e3_motor_not_running": "E3 Motor no funciona",
"e4_wrong_position": "E4 Posición incorrecta",
"e1_motor_fault": "E1 Fallo del motor",
"e5_communication_fault": "E5 Fallo de comunicación",
"e6_salt_sensor_fault": "E6 Fallo sensor de sal",
"e7_chlorine_sterilization_fault": "E7 Fallo esterilización cloro"
}
},
"error_code": {
"name": "Código de error"
},
"estimated_energy_consumption": {
"name": "Consumo estimado de energía"
},
"estimated_water_consumption": {
"name": "Consumo estimado de agua"
},
"fan_level": {
"name": "Nivel del ventilador"
},
@@ -452,6 +573,9 @@
"keep_warm_time": {
"name": "Tiempo de mantener caliente"
},
"left_salt": {
"name": "Sal restante"
},
"middle_compartment_remaining": {
"name": "Tiempo restante compartimento medio"
},
@@ -488,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "Temperatura establecida del frigorífico"
},
"regeneration_left_seconds": {
"name": "Segundos restantes de regeneración"
},
"remaining_days": {
"name": "Días restantes"
},
"right_flex_zone_actual_temp": {
"name": "Temperatura real de zona flexible derecha"
},
@@ -500,12 +630,18 @@
"rinse_level": {
"name": "Nivel de aclarado"
},
"salt_setting": {
"name": "Configuración de sal"
},
"seat_temperature": {
"name": "Temperatura del asiento"
},
"soak_time": {
"name": "Tiempo de remojo"
},
"soft_available": {
"name": "Agua suave disponible"
},
"softener": {
"name": "Suavizante"
},
@@ -551,6 +687,9 @@
"tvoc": {
"name": "COVT"
},
"wash_level": {
"name": "Recuento de aclarados"
},
"wash_strength": {
"name": "Potencia de lavado"
},
@@ -560,12 +699,66 @@
"water_consumption": {
"name": "Consumo de agua"
},
"water_consumption_average": {
"name": "Consumo promedio de agua"
},
"water_consumption_big": {
"name": "Consumo de agua"
},
"water_temperature": {
"name": "Temperatura del agua"
},
"water_level": {
"name": "Nivel de agua"
},
"wind": {
"name": "Viento"
},
"typeinfo": {
"name": "Información de tipo"
},
"use_days": {
"name": "Días de uso"
},
"elec_heat": {
"name": "Calefacción eléctrica"
},
"water_pump": {
"name": "Bomba de agua"
},
"four_way": {
"name": "Válvula de cuatro vías"
},
"back_water": {
"name": "Retorno de agua"
},
"sterilize": {
"name": "Esterilizar"
},
"disinfection_temperature": {
"name": "Temperatura de desinfección"
},
"max_temperature": {
"name": "Temperatura objetivo máxima"
},
"vacation_start_year": {
"name": "Año de inicio de vacaciones"
},
"vacation_start_month": {
"name": "Mes de inicio de vacaciones"
},
"vacation_start_day": {
"name": "Día de inicio de vacaciones"
},
"auto_sterilize_week": {
"name": "Semana de esterilización automática"
},
"auto_sterilize_hour": {
"name": "Hora de esterilización automática"
},
"auto_sterilize_minute": {
"name": "Minuto de esterilización automática"
},
"working_time": {
"name": "Tiempo de funcionamiento"
},
@@ -574,6 +767,42 @@
},
"variable_mode": {
"name": "Modo variable"
},
"compressor_frequency": {
"name": "Frecuencia del compresor"
},
"target_compressor_frequency": {
"name": "Frecuencia objetivo del compresor"
},
"compressor_current": {
"name": "Corriente del compresor"
},
"compressor_voltage": {
"name": "Voltaje del compresor"
},
"compressor_power": {
"name": "Potencia del compresor"
},
"indoor_coil_temperature": {
"name": "Temperatura del serpentín interior (T1)"
},
"evaporator_temperature": {
"name": "Temperatura del evaporador (T2)"
},
"outdoor_ambient_temperature": {
"name": "Temperatura ambiente exterior (T4)"
},
"discharge_pipe_temperature": {
"name": "Temperatura del tubo de descarga (TP)"
},
"indoor_fan_speed": {
"name": "Velocidad del ventilador interior"
},
"target_indoor_fan_speed": {
"name": "Velocidad objetivo del ventilador interior"
},
"velocity": {
"name": "Velocidad"
}
},
"switch": {
@@ -589,6 +818,12 @@
"breezeless": {
"name": "Sin corrientes"
},
"child_lock": {
"name": "Bloqueo infantil"
},
"cl_sterilization": {
"name": "Esterilización CL"
},
"comfort_mode": {
"name": "Modo confort"
},
@@ -598,6 +833,9 @@
"disinfect": {
"name": "Desinfectar"
},
"vacation_mode": {
"name": "Modo vacaciones"
},
"dry": {
"name": "Secado"
},
@@ -610,6 +848,9 @@
"eco_mode": {
"name": "Modo ECO"
},
"memory": {
"name": "Memo U"
},
"fast_dhw": {
"name": "ACS Rápido"
},
@@ -625,6 +866,9 @@
"indirect_wind": {
"name": "Viento indirecto"
},
"leak_water_protection": {
"name": "Protección contra fugas"
},
"light": {
"name": "Luz"
},
@@ -655,9 +899,15 @@
"prompt_tone": {
"name": "Tono de aviso"
},
"regeneration": {
"name": "Regeneración"
},
"water_pump": {
"name": "Bomba de agua"
},
"water_way": {
"name": "Vía de agua"
},
"screen_display": {
"name": "Visualización de pantalla"
},
@@ -673,6 +923,9 @@
"sleep_mode": {
"name": "Modo sueño"
},
"out_silent": {
"name": "Modo silencioso exterior"
},
"smart_eye": {
"name": "Ojo inteligente"
},
@@ -682,12 +935,18 @@
"smelly_sensor": {
"name": "Sensor de olores"
},
"soften": {
"name": "Suavizado"
},
"standby": {
"name": "En espera"
},
"start": {
"name": "Iniciar"
},
"sterilization": {
"name": "Esterilización"
},
"storage": {
"name": "Almacenamiento"
},
@@ -93,6 +93,9 @@
"cooking": {
"name": "Cooking"
},
"error_code": {
"name": "Code d'erreur"
},
"filter_change_reminder": {
"name": "Filter Change Reminder"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "Keep Warm"
},
"leak_water": {
"name": "Fuite d'eau"
},
"lid_status": {
"name": "Lid Status"
},
@@ -138,6 +144,21 @@
"middle_compartment_preheating": {
"name": "Middle Compartment Preheating"
},
"top_elec_heat": {
"name": "Chauffage électrique supérieur"
},
"bottom_elec_heat": {
"name": "Chauffage électrique inférieur"
},
"mute_effect": {
"name": "Effet silencieux"
},
"mute_status": {
"name": "État silencieux"
},
"multi_terminal": {
"name": "Multi-terminal"
},
"oilcup_full": {
"name": "Oil-cup Full"
},
@@ -153,12 +174,33 @@
"rinse_aid": {
"name": "Rinse Aid Shortage"
},
"rsj_stand_by": {
"name": "Veille"
},
"salt": {
"name": "Salt Shortage"
},
"seat_status": {
"name": "Seat Status"
},
"smart_grid": {
"name": "Réseau intelligent"
},
"eco": {
"name": "ECO"
},
"maintenance_reminder": {
"name": "Rappel d'entretien"
},
"maintain_warn": {
"name": "Avertissement d'entretien"
},
"order1_effect": {
"name": "Programmation 1 active"
},
"order2_effect": {
"name": "Programmation 2 active"
},
"status_dhw": {
"name": "DHW status"
},
@@ -206,6 +248,20 @@
},
"zone2_water_temp_mode": {
"name": "Zone2 Water-temperature Mode"
},
"microcrystal_fresh": {
"name": "Fraîcheur microcristal"
},
"electronic_smell": {
"name": "Désodorisation stérilisation"
},
"water_pump_running": {
"name": "Pompe à eau en marche"
}
},
"button": {
"start": {
"name": "Démarrer"
}
},
"climate": {
@@ -214,11 +270,24 @@
},
"climate_zone2": {
"name": "Zone2 Thermostat"
},
"climate_key": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Silencieux",
"full": "Maximum"
}
}
}
}
},
"fan": {
"fresh_air": {
"name": "Fresh Air"
},
"fresh_air_exhaust": {
"name": "Extraction d'air frais"
}
},
"lock": {
@@ -238,6 +307,21 @@
},
"water_temp_level": {
"name": "Water Temperature Level"
},
"fan_speed_percent": {
"name": "Pourcentage de vitesse du ventilateur"
},
"vacation_days": {
"name": "Jours de vacances"
},
"leak_water_protection_value": {
"name": "Valeur de protection contre les fuites"
},
"flushing_days": {
"name": "Jours de régénération"
},
"water_hardness": {
"name": "Dureté de l'eau"
}
},
"select": {
@@ -250,6 +334,25 @@
"fan_speed": {
"name": "Fan Speed"
},
"fresh_air_exhaust_mode": {
"name": "Vitesse d'extraction d'air frais",
"state": {
"off": "Arrêt",
"silent": "Faible",
"high": "Élevée",
"full": "Maximum"
}
},
"fresh_air_mode": {
"name": "Vitesse d'air frais",
"state": {
"off": "Arrêt",
"low": "Faible",
"medium": "Moyenne",
"high": "Élevée",
"full": "Maximum"
}
},
"mode": {
"name": "Mode"
},
@@ -259,6 +362,9 @@
"oscillation_mode": {
"name": "Oscillation Mode"
},
"rate_select": {
"name": "Power Rate Limit"
},
"screen_display": {
"name": "Screen Display"
},
@@ -268,8 +374,38 @@
"tilting_angle": {
"name": "Tilting Angle"
},
"wash_mode": {
"name": "Mode de lavage"
},
"water_level_set": {
"name": "Water Level Setting"
},
"wind_lr_angle": {
"name": "Flux d'air horizontal",
"state": {
"off": "Arrêt",
"left": "Gauche",
"left-mid": "Centre-gauche",
"middle": "Centre",
"right-mid": "Centre-droite",
"right": "Droite"
}
},
"wind_ud_angle": {
"name": "Flux d'air vertical",
"state": {
"off": "Arrêt",
"up": "Haut",
"up-mid": "Centre-haut",
"middle": "Centre",
"down-mid": "Centre-bas",
"down": "Bas"
}
}
},
"time": {
"timing_regeneration": {
"name": "Régénération programmée"
}
},
"sensor": {
@@ -309,15 +445,71 @@
"dehydration_time": {
"name": "dehydration time"
},
"dehydration_time_value": {
"name": "Valeur du temps d'essorage"
},
"temperature": {
"name": "Température"
},
"wash_time_value": {
"name": "Valeur du temps de lavage"
},
"stains": {
"name": "Taches"
},
"dirty_degree": {
"name": "Degré de saleté"
},
"detergent": {
"name": "detergent"
},
"intensity": {
"name": "Intensité"
},
"dryness_level": {
"name": "Niveau de séchage"
},
"dry_temperature": {
"name": "Température de séchage"
},
"door_warn": {
"name": "Alerte de porte"
},
"ai_switch": {
"name": "Interrupteur IA"
},
"material": {
"name": "Matériau"
},
"water_box": {
"name": "Réservoir d'eau"
},
"energy_consumption": {
"name": "Energy Consumption"
},
"error": {
"name": "Erreur",
"state": {
"no_error": "Aucune erreur",
"e1_position_not_found": "E1 Position introuvable",
"e2_photo_sensor_no_signal": "E2 Capteur photo sans signal",
"e3_motor_not_running": "E3 Moteur ne tourne pas",
"e4_wrong_position": "E4 Position incorrecte",
"e1_motor_fault": "E1 Panne moteur",
"e5_communication_fault": "E5 Panne de communication",
"e6_salt_sensor_fault": "E6 Panne capteur de sel",
"e7_chlorine_sterilization_fault": "E7 Panne stérilisation chlore"
}
},
"error_code": {
"name": "Error Code"
},
"estimated_energy_consumption": {
"name": "Consommation d'énergie estimée"
},
"estimated_water_consumption": {
"name": "Consommation d'eau estimée"
},
"fan_level": {
"name": "Fan level"
},
@@ -381,6 +573,9 @@
"keep_warm_time": {
"name": "Keep Warm Time"
},
"left_salt": {
"name": "Sel restant"
},
"middle_compartment_remaining": {
"name": "Middle Compartment Remaining"
},
@@ -409,7 +604,7 @@
"name": "Realtime Power"
},
"pmv": {
"name": "PMV"
"name": "Indice PMV"
},
"refrigerator_actual_temp": {
"name": "Refrigerator Actual Temperature"
@@ -417,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "Refrigerator Setting Temperature"
},
"regeneration_left_seconds": {
"name": "Secondes restantes de régénération"
},
"remaining_days": {
"name": "Jours restants"
},
"right_flex_zone_actual_temp": {
"name": "Right Flex Zone Actual Temperature"
},
@@ -429,12 +630,18 @@
"rinse_level": {
"name": "rinse level"
},
"salt_setting": {
"name": "Réglage du sel"
},
"seat_temperature": {
"name": "Seat Temperature"
},
"soak_time": {
"name": "soak time"
},
"soft_available": {
"name": "Eau douce disponible"
},
"softener": {
"name": "softener"
},
@@ -492,14 +699,110 @@
"water_consumption": {
"name": "Water Consumption"
},
"water_consumption_average": {
"name": "Consommation moyenne d'eau"
},
"water_consumption_big": {
"name": "Consommation d'eau"
},
"water_temperature": {
"name": "Water Temperature"
},
"water_level": {
"name": "Water Level"
},
"wind": {
"name": "Vent"
},
"typeinfo": {
"name": "Informations de type"
},
"use_days": {
"name": "Jours d'utilisation"
},
"elec_heat": {
"name": "Chauffage électrique"
},
"water_pump": {
"name": "Pompe à eau"
},
"four_way": {
"name": "Vanne quatre voies"
},
"back_water": {
"name": "Retour d'eau"
},
"sterilize": {
"name": "Stériliser"
},
"disinfection_temperature": {
"name": "Température de désinfection"
},
"max_temperature": {
"name": "Température cible maximale"
},
"vacation_start_year": {
"name": "Année de début des vacances"
},
"vacation_start_month": {
"name": "Mois de début des vacances"
},
"vacation_start_day": {
"name": "Jour de début des vacances"
},
"auto_sterilize_week": {
"name": "Semaine de stérilisation automatique"
},
"auto_sterilize_hour": {
"name": "Heure de stérilisation automatique"
},
"auto_sterilize_minute": {
"name": "Minute de stérilisation automatique"
},
"working_time": {
"name": "Working Time"
},
"humidity": {
"name": "Humidité"
},
"variable_mode": {
"name": "Mode variable"
},
"compressor_frequency": {
"name": "Fréquence du compresseur"
},
"target_compressor_frequency": {
"name": "Fréquence cible du compresseur"
},
"compressor_current": {
"name": "Courant du compresseur"
},
"compressor_voltage": {
"name": "Tension du compresseur"
},
"compressor_power": {
"name": "Puissance du compresseur"
},
"indoor_coil_temperature": {
"name": "Température de la batterie intérieure (T1)"
},
"evaporator_temperature": {
"name": "Température de l'évaporateur (T2)"
},
"outdoor_ambient_temperature": {
"name": "Température ambiante extérieure (T4)"
},
"discharge_pipe_temperature": {
"name": "Température du tuyau de refoulement (TP)"
},
"indoor_fan_speed": {
"name": "Vitesse du ventilateur intérieur"
},
"target_indoor_fan_speed": {
"name": "Vitesse cible du ventilateur intérieur"
},
"velocity": {
"name": "Vitesse"
}
},
"switch": {
@@ -515,6 +818,12 @@
"breezeless": {
"name": "Breezeless"
},
"child_lock": {
"name": "Verrouillage enfant"
},
"cl_sterilization": {
"name": "Stérilisation CL"
},
"comfort_mode": {
"name": "Comfort Mode"
},
@@ -524,12 +833,24 @@
"disinfect": {
"name": "Disinfect"
},
"vacation_mode": {
"name": "Mode vacances"
},
"dry": {
"name": "Dry"
},
"cold_water_single": {
"name": "Eau froide simple"
},
"cold_water_dot": {
"name": "Point d'eau froide"
},
"eco_mode": {
"name": "ECO Mode"
},
"memory": {
"name": "Mémo U"
},
"fast_dhw": {
"name": "Fast DHW"
},
@@ -545,6 +866,9 @@
"indirect_wind": {
"name": "Indirect Wind"
},
"leak_water_protection": {
"name": "Protection contre les fuites"
},
"light": {
"name": "Light"
},
@@ -575,9 +899,15 @@
"prompt_tone": {
"name": "Prompt Tone"
},
"regeneration": {
"name": "Régénération"
},
"water_pump": {
"name": "Pompe à eau"
},
"water_way": {
"name": "Voie d'eau"
},
"screen_display": {
"name": "Screen Display"
},
@@ -593,6 +923,9 @@
"sleep_mode": {
"name": "Sleep Mode"
},
"out_silent": {
"name": "Mode silencieux extérieur"
},
"smart_eye": {
"name": "Smart Eye"
},
@@ -602,17 +935,23 @@
"smelly_sensor": {
"name": "Smelly Sensor"
},
"soften": {
"name": "Adoucissement"
},
"standby": {
"name": "Standby"
},
"start": {
"name": "Start"
},
"sterilization": {
"name": "Stérilisation"
},
"storage": {
"name": "Storage"
},
"swing": {
"name": "swing"
"name": "Swing"
},
"swing_horizontal": {
"name": "Swing Horizontal"
@@ -621,10 +960,10 @@
"name": "Swing Vertical"
},
"self_clean": {
"name": "Self Clean"
"name": "Auto-nettoyage"
},
"sound": {
"name": "Sound"
"name": "Son"
},
"tbh": {
"name": "TBH"
@@ -93,6 +93,9 @@
"cooking": {
"name": "Cooking"
},
"error_code": {
"name": "Hibakód"
},
"filter_change_reminder": {
"name": "Filter Change Reminder"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "Keep Warm"
},
"leak_water": {
"name": "Vízszivárgás"
},
"lid_status": {
"name": "Lid Status"
},
@@ -138,6 +144,21 @@
"middle_compartment_preheating": {
"name": "Middle Compartment Preheating"
},
"top_elec_heat": {
"name": "Felső elektromos fűtés"
},
"bottom_elec_heat": {
"name": "Alsó elektromos fűtés"
},
"mute_effect": {
"name": "Némítás hatása"
},
"mute_status": {
"name": "Némítás állapota"
},
"multi_terminal": {
"name": "Több terminál"
},
"oilcup_full": {
"name": "Oil-cup Full"
},
@@ -153,12 +174,33 @@
"rinse_aid": {
"name": "Rinse Aid Shortage"
},
"rsj_stand_by": {
"name": "Készenlét"
},
"salt": {
"name": "Salt Shortage"
},
"seat_status": {
"name": "Seat Status"
},
"smart_grid": {
"name": "Okos hálózat"
},
"eco": {
"name": "ECO"
},
"maintenance_reminder": {
"name": "Karbantartási emlékeztető"
},
"maintain_warn": {
"name": "Karbantartási figyelmeztetés"
},
"order1_effect": {
"name": "1. ütemezés aktív"
},
"order2_effect": {
"name": "2. ütemezés aktív"
},
"status_dhw": {
"name": "DHW status"
},
@@ -206,6 +248,20 @@
},
"zone2_water_temp_mode": {
"name": "Zone2 Water-temperature Mode"
},
"microcrystal_fresh": {
"name": "Mikrokristályos frissítés"
},
"electronic_smell": {
"name": "Szagtalanító sterilizálás"
},
"water_pump_running": {
"name": "Vízszivattyú működik"
}
},
"button": {
"start": {
"name": "Indítás"
}
},
"climate": {
@@ -214,11 +270,24 @@
},
"climate_zone2": {
"name": "Zone2 Thermostat"
},
"climate_key": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Csendes",
"full": "Teljes"
}
}
}
}
},
"fan": {
"fresh_air": {
"name": "Fresh Air"
},
"fresh_air_exhaust": {
"name": "Frisslevegő elszívás"
}
},
"lock": {
@@ -238,6 +307,21 @@
},
"water_temp_level": {
"name": "Water Temperature Level"
},
"fan_speed_percent": {
"name": "Ventilátor sebesség százalék"
},
"vacation_days": {
"name": "Vakáció napok"
},
"leak_water_protection_value": {
"name": "Vízszivárgás védelem értéke"
},
"flushing_days": {
"name": "Regenerálási napok"
},
"water_hardness": {
"name": "Víz keménység"
}
},
"select": {
@@ -250,6 +334,25 @@
"fan_speed": {
"name": "Fan Speed"
},
"fresh_air_exhaust_mode": {
"name": "Frisslevegő elszívás sebessége",
"state": {
"off": "Ki",
"silent": "Alacsony",
"high": "Magas",
"full": "Teljes"
}
},
"fresh_air_mode": {
"name": "Frisslevegő sebessége",
"state": {
"off": "Ki",
"low": "Alacsony",
"medium": "Közepes",
"high": "Magas",
"full": "Teljes"
}
},
"mode": {
"name": "Mode"
},
@@ -259,6 +362,9 @@
"oscillation_mode": {
"name": "Oscillation Mode"
},
"rate_select": {
"name": "Power Rate Limit"
},
"screen_display": {
"name": "Screen Display"
},
@@ -268,8 +374,38 @@
"tilting_angle": {
"name": "Tilting Angle"
},
"wash_mode": {
"name": "Mosási mód"
},
"water_level_set": {
"name": "Water Level Setting"
},
"wind_lr_angle": {
"name": "Légáramlás vízszintes",
"state": {
"off": "Ki",
"left": "Bal",
"left-mid": "Bal-közép",
"middle": "Közép",
"right-mid": "Jobb-közép",
"right": "Jobb"
}
},
"wind_ud_angle": {
"name": "Légáramlás függőleges",
"state": {
"off": "Ki",
"up": "Fel",
"up-mid": "Fel-közép",
"middle": "Közép",
"down-mid": "Le-közép",
"down": "Le"
}
}
},
"time": {
"timing_regeneration": {
"name": "Időzített regenerálás"
}
},
"sensor": {
@@ -309,15 +445,71 @@
"dehydration_time": {
"name": "dehydration time"
},
"dehydration_time_value": {
"name": "Víztelenítési idő értéke"
},
"temperature": {
"name": "Hőmérséklet"
},
"wash_time_value": {
"name": "Mosási idő értéke"
},
"stains": {
"name": "Foltok"
},
"dirty_degree": {
"name": "Szennyezettség mértéke"
},
"detergent": {
"name": "detergent"
},
"intensity": {
"name": "Intenzitás"
},
"dryness_level": {
"name": "Szárazsági szint"
},
"dry_temperature": {
"name": "Szárítási hőmérséklet"
},
"door_warn": {
"name": "Ajtó figyelmeztetés"
},
"ai_switch": {
"name": "AI kapcsoló"
},
"material": {
"name": "Anyag"
},
"water_box": {
"name": "Víztartály"
},
"energy_consumption": {
"name": "Energy Consumption"
},
"error": {
"name": "Hiba",
"state": {
"no_error": "Nincs hiba",
"e1_position_not_found": "E1 Pozíció nem található",
"e2_photo_sensor_no_signal": "E2 Fotóérzékelő nincs jel",
"e3_motor_not_running": "E3 Motor nem fut",
"e4_wrong_position": "E4 Hibás pozíció",
"e1_motor_fault": "E1 Motor hiba",
"e5_communication_fault": "E5 Kommunikációs hiba",
"e6_salt_sensor_fault": "E6 Sóérzékelő hiba",
"e7_chlorine_sterilization_fault": "E7 Klór sterilizációs hiba"
}
},
"error_code": {
"name": "Error Code"
},
"estimated_energy_consumption": {
"name": "Becsült energiafogyasztás"
},
"estimated_water_consumption": {
"name": "Becsült vízfogyasztás"
},
"fan_level": {
"name": "Fan level"
},
@@ -381,6 +573,9 @@
"keep_warm_time": {
"name": "Keep Warm Time"
},
"left_salt": {
"name": "Maradék só"
},
"middle_compartment_remaining": {
"name": "Middle Compartment Remaining"
},
@@ -417,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "Refrigerator Setting Temperature"
},
"regeneration_left_seconds": {
"name": "Regenerálás hátralévő másodperc"
},
"remaining_days": {
"name": "Hátralévő napok"
},
"right_flex_zone_actual_temp": {
"name": "Right Flex Zone Actual Temperature"
},
@@ -429,12 +630,18 @@
"rinse_level": {
"name": "rinse level"
},
"salt_setting": {
"name": "Só beállítása"
},
"seat_temperature": {
"name": "Seat Temperature"
},
"soak_time": {
"name": "soak time"
},
"soft_available": {
"name": "Elérhető lágy víz"
},
"softener": {
"name": "softener"
},
@@ -492,14 +699,110 @@
"water_consumption": {
"name": "Water Consumption"
},
"water_consumption_average": {
"name": "Átlagos vízfogyasztás"
},
"water_consumption_big": {
"name": "Vízfogyasztás"
},
"water_temperature": {
"name": "Water Temperature"
},
"water_level": {
"name": "Water Level"
},
"wind": {
"name": "Szél"
},
"typeinfo": {
"name": "Típusinformáció"
},
"use_days": {
"name": "Használati napok"
},
"elec_heat": {
"name": "Elektromos fűtés"
},
"water_pump": {
"name": "Vízszivattyú"
},
"four_way": {
"name": "Négyjáratú szelep"
},
"back_water": {
"name": "Visszatérő víz"
},
"sterilize": {
"name": "Sterilizálás"
},
"disinfection_temperature": {
"name": "Fertőtlenítési hőmérséklet"
},
"max_temperature": {
"name": "Maximális cél hőmérséklet"
},
"vacation_start_year": {
"name": "Vakáció kezdő év"
},
"vacation_start_month": {
"name": "Vakáció kezdő hónap"
},
"vacation_start_day": {
"name": "Vakáció kezdő nap"
},
"auto_sterilize_week": {
"name": "Automatikus sterilizálás hét"
},
"auto_sterilize_hour": {
"name": "Automatikus sterilizálás óra"
},
"auto_sterilize_minute": {
"name": "Automatikus sterilizálás perc"
},
"working_time": {
"name": "Working Time"
},
"humidity": {
"name": "Páratartalom"
},
"variable_mode": {
"name": "Változó mód"
},
"compressor_frequency": {
"name": "Kompresszor frekvencia"
},
"target_compressor_frequency": {
"name": "Cél kompresszor frekvencia"
},
"compressor_current": {
"name": "Kompresszor áramerősség"
},
"compressor_voltage": {
"name": "Kompresszor feszültség"
},
"compressor_power": {
"name": "Kompresszor teljesítmény"
},
"indoor_coil_temperature": {
"name": "Beltéri hőcserélő hőmérséklet (T1)"
},
"evaporator_temperature": {
"name": "Párologtató hőmérséklet (T2)"
},
"outdoor_ambient_temperature": {
"name": "Kültéri környezeti hőmérséklet (T4)"
},
"discharge_pipe_temperature": {
"name": "Nyomócső hőmérséklet (TP)"
},
"indoor_fan_speed": {
"name": "Beltéri ventilátor sebesség"
},
"target_indoor_fan_speed": {
"name": "Cél beltéri ventilátor sebesség"
},
"velocity": {
"name": "Sebesség"
}
},
"switch": {
@@ -515,6 +818,12 @@
"breezeless": {
"name": "Breezeless"
},
"child_lock": {
"name": "Gyermekzár"
},
"cl_sterilization": {
"name": "CL sterilizálás"
},
"comfort_mode": {
"name": "Comfort Mode"
},
@@ -524,12 +833,24 @@
"disinfect": {
"name": "Disinfect"
},
"vacation_mode": {
"name": "Vakáció mód"
},
"dry": {
"name": "Dry"
},
"cold_water_single": {
"name": "Hideg víz egyszeri"
},
"cold_water_dot": {
"name": "Hideg víz pont"
},
"eco_mode": {
"name": "ECO Mode"
},
"memory": {
"name": "Memo U"
},
"fast_dhw": {
"name": "Fast DHW"
},
@@ -545,6 +866,9 @@
"indirect_wind": {
"name": "Indirect Wind"
},
"leak_water_protection": {
"name": "Védelem a vízszivárgás ellen"
},
"light": {
"name": "Light"
},
@@ -575,9 +899,15 @@
"prompt_tone": {
"name": "Prompt Tone"
},
"regeneration": {
"name": "Regenerálás"
},
"water_pump": {
"name": "Vízszivattyú"
},
"water_way": {
"name": "Vízi út"
},
"screen_display": {
"name": "Screen Display"
},
@@ -593,6 +923,9 @@
"sleep_mode": {
"name": "Sleep Mode"
},
"out_silent": {
"name": "Kültéri csendes mód"
},
"smart_eye": {
"name": "Smart Eye"
},
@@ -602,12 +935,18 @@
"smelly_sensor": {
"name": "Smelly Sensor"
},
"soften": {
"name": "Lágyítás"
},
"standby": {
"name": "Standby"
},
"start": {
"name": "Start"
},
"sterilization": {
"name": "Sterilizálás"
},
"storage": {
"name": "Storage"
},
@@ -93,6 +93,9 @@
"cooking": {
"name": "Приготовление"
},
"error_code": {
"name": "Код ошибки"
},
"filter_change_reminder": {
"name": "Требуется замена фильтра"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "Поддержание температуры"
},
"leak_water": {
"name": "Протечка воды"
},
"lid_status": {
"name": "Крышка"
},
@@ -138,6 +144,21 @@
"middle_compartment_preheating": {
"name": "Middle Compartment Preheating"
},
"top_elec_heat": {
"name": "Верхний электронагрев"
},
"bottom_elec_heat": {
"name": "Нижний электронагрев"
},
"mute_effect": {
"name": "Эффект тихого режима"
},
"mute_status": {
"name": "Статус тихого режима"
},
"multi_terminal": {
"name": "Мультитерминал"
},
"oilcup_full": {
"name": "Oil-cup Full"
},
@@ -153,12 +174,33 @@
"rinse_aid": {
"name": "Нехватка ополаскивателя"
},
"rsj_stand_by": {
"name": "Ожидание"
},
"salt": {
"name": "Соль"
},
"seat_status": {
"name": "Сиденье"
},
"smart_grid": {
"name": "Умная сеть"
},
"eco": {
"name": "ECO"
},
"maintenance_reminder": {
"name": "Напоминание об обслуживании"
},
"maintain_warn": {
"name": "Предупреждение об обслуживании"
},
"order1_effect": {
"name": "Расписание 1 активно"
},
"order2_effect": {
"name": "Расписание 2 активно"
},
"status_dhw": {
"name": "DHW status"
},
@@ -206,6 +248,20 @@
},
"zone2_water_temp_mode": {
"name": "Zone2 Water-temperature Mode"
},
"microcrystal_fresh": {
"name": "Микрокристаллическая свежесть"
},
"electronic_smell": {
"name": "Дезодорация и стерилизация"
},
"water_pump_running": {
"name": "Работа водяного насоса"
}
},
"button": {
"start": {
"name": "Старт"
}
},
"climate": {
@@ -214,11 +270,24 @@
},
"climate_zone2": {
"name": "Zone2 Thermostat"
},
"climate_key": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Тихий",
"full": "Полная"
}
}
}
}
},
"fan": {
"fresh_air": {
"name": "Fresh Air"
},
"fresh_air_exhaust": {
"name": "Вытяжка свежего воздуха"
}
},
"lock": {
@@ -238,6 +307,21 @@
},
"water_temp_level": {
"name": "Water Temperature Level"
},
"fan_speed_percent": {
"name": "Скорость вентилятора (%)"
},
"vacation_days": {
"name": "Дни отпуска"
},
"leak_water_protection_value": {
"name": "Значение защиты от протечек"
},
"flushing_days": {
"name": "Дни регенерации"
},
"water_hardness": {
"name": "Жёсткость воды"
}
},
"select": {
@@ -250,6 +334,25 @@
"fan_speed": {
"name": "Скорость"
},
"fresh_air_exhaust_mode": {
"name": "Скорость вытяжки свежего воздуха",
"state": {
"off": "Выкл",
"silent": "Низкая",
"high": "Высокая",
"full": "Максимальная"
}
},
"fresh_air_mode": {
"name": "Скорость свежего воздуха",
"state": {
"off": "Выкл",
"low": "Низкая",
"medium": "Средняя",
"high": "Высокая",
"full": "Максимальная"
}
},
"mode": {
"name": "Режим"
},
@@ -259,6 +362,9 @@
"oscillation_mode": {
"name": "Oscillation Mode"
},
"rate_select": {
"name": "Ограничение мощности"
},
"screen_display": {
"name": "Screen Display"
},
@@ -268,8 +374,38 @@
"tilting_angle": {
"name": "Угол поворота"
},
"wash_mode": {
"name": "Режим мойки"
},
"water_level_set": {
"name": "Water Level Setting"
},
"wind_lr_angle": {
"name": "Горизонтальный поток воздуха",
"state": {
"off": "Выкл",
"left": "Влево",
"left-mid": "Влево-центр",
"middle": "Центр",
"right-mid": "Вправо-центр",
"right": "Вправо"
}
},
"wind_ud_angle": {
"name": "Вертикальный поток воздуха",
"state": {
"off": "Выкл",
"up": "Вверх",
"up-mid": "Вверх-центр",
"middle": "Центр",
"down-mid": "Вниз-центр",
"down": "Вниз"
}
}
},
"time": {
"timing_regeneration": {
"name": "Регенерация по таймеру"
}
},
"sensor": {
@@ -312,6 +448,9 @@
"dehydration_time_value": {
"name": "Dehydration time value"
},
"temperature": {
"name": "Температура"
},
"wash_time_value": {
"name": "Wash time value"
},
@@ -348,9 +487,29 @@
"energy_consumption": {
"name": "Энергопотребление"
},
"error": {
"name": "Ошибка",
"state": {
"no_error": "Нет ошибок",
"e1_position_not_found": "E1 Позиция не найдена",
"e2_photo_sensor_no_signal": "E2 Датчик не даёт сигнал",
"e3_motor_not_running": "E3 Двигатель не работает",
"e4_wrong_position": "E4 Неправильная позиция",
"e1_motor_fault": "E1 Неисправность двигателя",
"e5_communication_fault": "E5 Ошибка связи",
"e6_salt_sensor_fault": "E6 Неисправность датчика соли",
"e7_chlorine_sterilization_fault": "E7 Неисправность хлорной стерилизации"
}
},
"error_code": {
"name": "Код ошибки"
},
"estimated_energy_consumption": {
"name": "Расчётное энергопотребление"
},
"estimated_water_consumption": {
"name": "Расчётный расход воды"
},
"fan_level": {
"name": "Fan level"
},
@@ -414,6 +573,9 @@
"keep_warm_time": {
"name": "Keep Warm Time"
},
"left_salt": {
"name": "Остаток соли"
},
"middle_compartment_remaining": {
"name": "Middle Compartment Remaining"
},
@@ -450,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "Refrigerator Setting Temperature"
},
"regeneration_left_seconds": {
"name": "Оставшиеся секунды регенерации"
},
"remaining_days": {
"name": "Оставшиеся дни"
},
"right_flex_zone_actual_temp": {
"name": "Right Flex Zone Actual Temperature"
},
@@ -462,12 +630,18 @@
"rinse_level": {
"name": "Уровень ополаскивателя"
},
"salt_setting": {
"name": "Настройка соли"
},
"seat_temperature": {
"name": "Температура сиденья"
},
"soak_time": {
"name": "Время замачивания"
},
"soft_available": {
"name": "Доступная мягкая вода"
},
"softener": {
"name": "Умягчитель"
},
@@ -513,6 +687,9 @@
"tvoc": {
"name": "TVOC"
},
"wash_level": {
"name": "Количество полосканий"
},
"wash_strength": {
"name": "Wash strength"
},
@@ -522,14 +699,110 @@
"water_consumption": {
"name": "Water Consumption"
},
"water_consumption_average": {
"name": "Средний расход воды"
},
"water_consumption_big": {
"name": "Потребление воды"
},
"water_temperature": {
"name": "Температура воды"
},
"water_level": {
"name": "Уровень воды"
},
"wind": {
"name": "Поток воздуха"
},
"typeinfo": {
"name": "Информация о типе"
},
"use_days": {
"name": "Дни использования"
},
"elec_heat": {
"name": "Электронагрев"
},
"water_pump": {
"name": "Водяной насос"
},
"four_way": {
"name": "Четырёхходовой клапан"
},
"back_water": {
"name": "Обратная вода"
},
"sterilize": {
"name": "Стерилизация"
},
"disinfection_temperature": {
"name": "Температура дезинфекции"
},
"max_temperature": {
"name": "Максимальная целевая температура"
},
"vacation_start_year": {
"name": "Год начала отпуска"
},
"vacation_start_month": {
"name": "Месяц начала отпуска"
},
"vacation_start_day": {
"name": "День начала отпуска"
},
"auto_sterilize_week": {
"name": "День недели автостерилизации"
},
"auto_sterilize_hour": {
"name": "Час автостерилизации"
},
"auto_sterilize_minute": {
"name": "Минута автостерилизации"
},
"working_time": {
"name": "Время работы"
},
"humidity": {
"name": "Влажность"
},
"variable_mode": {
"name": "Переменный режим"
},
"compressor_frequency": {
"name": "Частота компрессора"
},
"target_compressor_frequency": {
"name": "Целевая частота компрессора"
},
"compressor_current": {
"name": "Ток компрессора"
},
"compressor_voltage": {
"name": "Напряжение компрессора"
},
"compressor_power": {
"name": "Мощность компрессора"
},
"indoor_coil_temperature": {
"name": "Температура внутреннего теплообменника (T1)"
},
"evaporator_temperature": {
"name": "Температура испарителя (T2)"
},
"outdoor_ambient_temperature": {
"name": "Наружная температура окружающей среды (T4)"
},
"discharge_pipe_temperature": {
"name": "Температура нагнетательной трубы (TP)"
},
"indoor_fan_speed": {
"name": "Скорость внутреннего вентилятора"
},
"target_indoor_fan_speed": {
"name": "Целевая скорость внутреннего вентилятора"
},
"velocity": {
"name": "Скорость"
}
},
"switch": {
@@ -545,6 +818,12 @@
"breezeless": {
"name": "Breezeless"
},
"child_lock": {
"name": "Блокировка от детей"
},
"cl_sterilization": {
"name": "CL-стерилизация"
},
"comfort_mode": {
"name": "Comfort Mode"
},
@@ -554,12 +833,24 @@
"disinfect": {
"name": "Дезинфекция"
},
"vacation_mode": {
"name": "Режим отпуска"
},
"dry": {
"name": "Dry"
},
"cold_water_single": {
"name": "Холодная вода (однократно)"
},
"cold_water_dot": {
"name": "Холодная вода (порционно)"
},
"eco_mode": {
"name": "Эко-режим"
},
"memory": {
"name": "Memo U"
},
"fast_dhw": {
"name": "Fast DHW"
},
@@ -575,6 +866,9 @@
"indirect_wind": {
"name": "Indirect Wind"
},
"leak_water_protection": {
"name": "Защита от протечек"
},
"light": {
"name": "Свет"
},
@@ -605,9 +899,15 @@
"prompt_tone": {
"name": "Prompt Tone"
},
"regeneration": {
"name": "Регенерация"
},
"water_pump": {
"name": "Водяной насос"
},
"water_way": {
"name": "Водный путь"
},
"screen_display": {
"name": "Screen Display"
},
@@ -623,6 +923,9 @@
"sleep_mode": {
"name": "Sleep Mode"
},
"out_silent": {
"name": "Тихий режим наружного блока"
},
"smart_eye": {
"name": "Smart Eye"
},
@@ -632,12 +935,18 @@
"smelly_sensor": {
"name": "Smelly Sensor"
},
"soften": {
"name": "Умягчение"
},
"standby": {
"name": "Ожидание"
},
"start": {
"name": "Старт"
},
"sterilization": {
"name": "Стерилизация"
},
"storage": {
"name": "Storage"
},
@@ -93,6 +93,9 @@
"cooking": {
"name": "Cooking"
},
"error_code": {
"name": "Kód chyby"
},
"filter_change_reminder": {
"name": "Filter Change Reminder"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "Keep Warm"
},
"leak_water": {
"name": "Únik vody"
},
"lid_status": {
"name": "Lid Status"
},
@@ -138,6 +144,21 @@
"middle_compartment_preheating": {
"name": "Middle Compartment Preheating"
},
"top_elec_heat": {
"name": "Horné elektrické vykurovanie"
},
"bottom_elec_heat": {
"name": "Dolné elektrické vykurovanie"
},
"mute_effect": {
"name": "Účinok stíšenia"
},
"mute_status": {
"name": "Stav stíšenia"
},
"multi_terminal": {
"name": "Viacero terminálov"
},
"oilcup_full": {
"name": "Oil-cup Full"
},
@@ -153,12 +174,33 @@
"rinse_aid": {
"name": "Rinse Aid Shortage"
},
"rsj_stand_by": {
"name": "Pohotovostný režim"
},
"salt": {
"name": "Salt Shortage"
},
"seat_status": {
"name": "Seat Status"
},
"smart_grid": {
"name": "Inteligentná sieť"
},
"eco": {
"name": "ECO"
},
"maintenance_reminder": {
"name": "Pripomienka údržby"
},
"maintain_warn": {
"name": "Upozornenie na údržbu"
},
"order1_effect": {
"name": "Plán 1 aktívny"
},
"order2_effect": {
"name": "Plán 2 aktívny"
},
"status_dhw": {
"name": "DHW status"
},
@@ -206,6 +248,20 @@
},
"zone2_water_temp_mode": {
"name": "Zone2 Water-temperature Mode"
},
"microcrystal_fresh": {
"name": "Mikrokryštalická sviežosť"
},
"electronic_smell": {
"name": "Dezodorácia a sterilizácia"
},
"water_pump_running": {
"name": "Vodné čerpadlo beží"
}
},
"button": {
"start": {
"name": "Štart"
}
},
"climate": {
@@ -214,11 +270,24 @@
},
"climate_zone2": {
"name": "Zone2 Thermostat"
},
"climate_key": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Tichý",
"full": "Plný"
}
}
}
}
},
"fan": {
"fresh_air": {
"name": "Fresh Air"
},
"fresh_air_exhaust": {
"name": "Odvod čerstvého vzduchu"
}
},
"lock": {
@@ -238,6 +307,21 @@
},
"water_temp_level": {
"name": "Water Temperature Level"
},
"fan_speed_percent": {
"name": "Percento rýchlosti ventilátora"
},
"vacation_days": {
"name": "Dni dovolenky"
},
"leak_water_protection_value": {
"name": "Hodnota ochrany proti úniku vody"
},
"flushing_days": {
"name": "Dni regenerácie"
},
"water_hardness": {
"name": "Tvrdosť vody"
}
},
"select": {
@@ -250,6 +334,25 @@
"fan_speed": {
"name": "Fan Speed"
},
"fresh_air_exhaust_mode": {
"name": "Rýchlosť odvodu čerstvého vzduchu",
"state": {
"off": "Vypnuté",
"silent": "Nízka",
"high": "Vysoká",
"full": "Plná"
}
},
"fresh_air_mode": {
"name": "Rýchlosť čerstvého vzduchu",
"state": {
"off": "Vypnuté",
"low": "Nízka",
"medium": "Stredná",
"high": "Vysoká",
"full": "Plná"
}
},
"mode": {
"name": "Mode"
},
@@ -259,6 +362,9 @@
"oscillation_mode": {
"name": "Oscillation Mode"
},
"rate_select": {
"name": "Power Rate Limit"
},
"screen_display": {
"name": "Screen Display"
},
@@ -268,8 +374,38 @@
"tilting_angle": {
"name": "Tilting Angle"
},
"wash_mode": {
"name": "Režim prania"
},
"water_level_set": {
"name": "Water Level Setting"
},
"wind_lr_angle": {
"name": "Vodorovné prúdenie vzduchu",
"state": {
"off": "Vypnuté",
"left": "Vľavo",
"left-mid": "Vľavo-stred",
"middle": "Stred",
"right-mid": "Vpravo-stred",
"right": "Vpravo"
}
},
"wind_ud_angle": {
"name": "Zvislé prúdenie vzduchu",
"state": {
"off": "Vypnuté",
"up": "Hore",
"up-mid": "Hore-stred",
"middle": "Stred",
"down-mid": "Dole-stred",
"down": "Dole"
}
}
},
"time": {
"timing_regeneration": {
"name": "Časovaná regenerácia"
}
},
"sensor": {
@@ -309,15 +445,71 @@
"dehydration_time": {
"name": "dehydration time"
},
"dehydration_time_value": {
"name": "Hodnota času odstreďovania"
},
"temperature": {
"name": "Teplota"
},
"wash_time_value": {
"name": "Hodnota času prania"
},
"stains": {
"name": "Škvrny"
},
"dirty_degree": {
"name": "Stupeň znečistenia"
},
"detergent": {
"name": "detergent"
},
"intensity": {
"name": "Intenzita"
},
"dryness_level": {
"name": "Úroveň suchosti"
},
"dry_temperature": {
"name": "Teplota sušenia"
},
"door_warn": {
"name": "Upozornenie na dvere"
},
"ai_switch": {
"name": "AI prepínač"
},
"material": {
"name": "Materiál"
},
"water_box": {
"name": "Nádržka na vodu"
},
"energy_consumption": {
"name": "Energy Consumption"
},
"error": {
"name": "Chyba",
"state": {
"no_error": "Bez chyby",
"e1_position_not_found": "E1 Pozícia nenájdená",
"e2_photo_sensor_no_signal": "E2 Fotosenzor bez signálu",
"e3_motor_not_running": "E3 Motor nebeží",
"e4_wrong_position": "E4 Nesprávna pozícia",
"e1_motor_fault": "E1 Porucha motora",
"e5_communication_fault": "E5 Porucha komunikácie",
"e6_salt_sensor_fault": "E6 Porucha senzora soli",
"e7_chlorine_sterilization_fault": "E7 Porucha chlórovej sterilizácie"
}
},
"error_code": {
"name": "Error Code"
},
"estimated_energy_consumption": {
"name": "Odhadovaná spotreba energie"
},
"estimated_water_consumption": {
"name": "Odhadovaná spotreba vody"
},
"fan_level": {
"name": "Fan level"
},
@@ -381,6 +573,9 @@
"keep_warm_time": {
"name": "Keep Warm Time"
},
"left_salt": {
"name": "Zvyšná soľ"
},
"middle_compartment_remaining": {
"name": "Middle Compartment Remaining"
},
@@ -417,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "Refrigerator Setting Temperature"
},
"regeneration_left_seconds": {
"name": "Zostávajúce sekundy regenerácie"
},
"remaining_days": {
"name": "Zostávajúce dni"
},
"right_flex_zone_actual_temp": {
"name": "Right Flex Zone Actual Temperature"
},
@@ -429,12 +630,18 @@
"rinse_level": {
"name": "rinse level"
},
"salt_setting": {
"name": "Nastavenie soli"
},
"seat_temperature": {
"name": "Seat Temperature"
},
"soak_time": {
"name": "soak time"
},
"soft_available": {
"name": "Dostupná mäkká voda"
},
"softener": {
"name": "softener"
},
@@ -492,14 +699,110 @@
"water_consumption": {
"name": "Water Consumption"
},
"water_consumption_average": {
"name": "Priemerná spotreba vody"
},
"water_consumption_big": {
"name": "Spotreba vody"
},
"water_temperature": {
"name": "Water Temperature"
},
"water_level": {
"name": "Water Level"
},
"wind": {
"name": "Vietor"
},
"typeinfo": {
"name": "Informácie o type"
},
"use_days": {
"name": "Dni používania"
},
"elec_heat": {
"name": "Elektrické vykurovanie"
},
"water_pump": {
"name": "Vodné čerpadlo"
},
"four_way": {
"name": "Štvorcestný ventil"
},
"back_water": {
"name": "Spätná voda"
},
"sterilize": {
"name": "Sterilizácia"
},
"disinfection_temperature": {
"name": "Teplota dezinfekcie"
},
"max_temperature": {
"name": "Maximálna cieľová teplota"
},
"vacation_start_year": {
"name": "Rok začiatku dovolenky"
},
"vacation_start_month": {
"name": "Mesiac začiatku dovolenky"
},
"vacation_start_day": {
"name": "Deň začiatku dovolenky"
},
"auto_sterilize_week": {
"name": "Týždeň automatickej sterilizácie"
},
"auto_sterilize_hour": {
"name": "Hodina automatickej sterilizácie"
},
"auto_sterilize_minute": {
"name": "Minúta automatickej sterilizácie"
},
"working_time": {
"name": "Working Time"
},
"humidity": {
"name": "Vlhkosť"
},
"variable_mode": {
"name": "Variabilný režim"
},
"compressor_frequency": {
"name": "Frekvencia kompresora"
},
"target_compressor_frequency": {
"name": "Cieľová frekvencia kompresora"
},
"compressor_current": {
"name": "Prúd kompresora"
},
"compressor_voltage": {
"name": "Napätie kompresora"
},
"compressor_power": {
"name": "Výkon kompresora"
},
"indoor_coil_temperature": {
"name": "Teplota vnútorného výmenníka (T1)"
},
"evaporator_temperature": {
"name": "Teplota výparníka (T2)"
},
"outdoor_ambient_temperature": {
"name": "Vonkajšia teplota okolia (T4)"
},
"discharge_pipe_temperature": {
"name": "Teplota výtlačného potrubia (TP)"
},
"indoor_fan_speed": {
"name": "Rýchlosť vnútorného ventilátora"
},
"target_indoor_fan_speed": {
"name": "Cieľová rýchlosť vnútorného ventilátora"
},
"velocity": {
"name": "Rýchlosť"
}
},
"switch": {
@@ -515,6 +818,12 @@
"breezeless": {
"name": "Breezeless"
},
"child_lock": {
"name": "Detský zámok"
},
"cl_sterilization": {
"name": "CL sterilizácia"
},
"comfort_mode": {
"name": "Comfort Mode"
},
@@ -524,12 +833,24 @@
"disinfect": {
"name": "Disinfect"
},
"vacation_mode": {
"name": "Režim dovolenky"
},
"dry": {
"name": "Dry"
},
"cold_water_single": {
"name": "Studená voda jednorazovo"
},
"cold_water_dot": {
"name": "Studená voda bodovo"
},
"eco_mode": {
"name": "ECO Mode"
},
"memory": {
"name": "Memo U"
},
"fast_dhw": {
"name": "Fast DHW"
},
@@ -545,6 +866,9 @@
"indirect_wind": {
"name": "Indirect Wind"
},
"leak_water_protection": {
"name": "Ochrana proti úniku vody"
},
"light": {
"name": "Light"
},
@@ -575,9 +899,15 @@
"prompt_tone": {
"name": "Prompt Tone"
},
"regeneration": {
"name": "Regenerácia"
},
"water_pump": {
"name": "Vodné čerpadlo"
},
"water_way": {
"name": "Vodná cesta"
},
"screen_display": {
"name": "Screen Display"
},
@@ -593,6 +923,9 @@
"sleep_mode": {
"name": "Sleep Mode"
},
"out_silent": {
"name": "Vonkajší tichý režim"
},
"smart_eye": {
"name": "Smart Eye"
},
@@ -602,12 +935,18 @@
"smelly_sensor": {
"name": "Smelly Sensor"
},
"soften": {
"name": "Zmäkčovanie"
},
"standby": {
"name": "Standby"
},
"start": {
"name": "Start"
},
"sterilization": {
"name": "Sterilizácia"
},
"storage": {
"name": "Storage"
},
@@ -93,6 +93,9 @@
"cooking": {
"name": "烹饪中"
},
"error_code": {
"name": "错误代码"
},
"filter_change_reminder": {
"name": "滤芯更换提醒"
},
@@ -126,6 +129,9 @@
"keep_warm": {
"name": "保温"
},
"leak_water": {
"name": "漏水报警"
},
"lid_status": {
"name": "盖子状态"
},
@@ -138,6 +144,21 @@
"middle_compartment_preheating": {
"name": "中层预热"
},
"top_elec_heat": {
"name": "顶部电加热"
},
"bottom_elec_heat": {
"name": "底部电加热"
},
"mute_effect": {
"name": "静音效果"
},
"mute_status": {
"name": "静音状态"
},
"multi_terminal": {
"name": "多终端"
},
"oilcup_full": {
"name": "油杯满提示"
},
@@ -153,12 +174,33 @@
"rinse_aid": {
"name": "漂洗剂不足"
},
"rsj_stand_by": {
"name": "待机状态"
},
"salt": {
"name": "软水盐不足"
},
"seat_status": {
"name": "入座状态"
},
"smart_grid": {
"name": "智能电网"
},
"eco": {
"name": "ECO"
},
"maintenance_reminder": {
"name": "维护提醒"
},
"maintain_warn": {
"name": "维护警告"
},
"order1_effect": {
"name": "定时1生效"
},
"order2_effect": {
"name": "定时2生效"
},
"status_dhw": {
"name": "DHW状态"
},
@@ -212,6 +254,14 @@
},
"electronic_smell": {
"name": "净味杀菌"
},
"water_pump_running": {
"name": "水泵运行"
}
},
"button": {
"start": {
"name": "启动"
}
},
"climate": {
@@ -235,6 +285,9 @@
"fan": {
"fresh_air": {
"name": "新风"
},
"fresh_air_exhaust": {
"name": "排风"
}
},
"lock": {
@@ -257,6 +310,18 @@
},
"fan_speed_percent": {
"name": "风速百分比"
},
"vacation_days": {
"name": "假期天数"
},
"leak_water_protection_value": {
"name": "漏水保护阈值"
},
"flushing_days": {
"name": "再生天数"
},
"water_hardness": {
"name": "进水硬度"
}
},
"select": {
@@ -269,6 +334,25 @@
"fan_speed": {
"name": "风速设定"
},
"fresh_air_exhaust_mode": {
"name": "排风风速",
"state": {
"off": "关闭",
"silent": "低速",
"high": "高速",
"full": "强劲"
}
},
"fresh_air_mode": {
"name": "新风风速",
"state": {
"off": "关闭",
"low": "低速",
"medium": "中速",
"high": "高速",
"full": "强劲"
}
},
"mode": {
"name": "模式"
},
@@ -278,6 +362,9 @@
"oscillation_mode": {
"name": "水平摆头模式"
},
"rate_select": {
"name": "功率限制"
},
"screen_display": {
"name": "屏幕显示"
},
@@ -287,6 +374,9 @@
"tilting_angle": {
"name": "垂直摆头角度"
},
"wash_mode": {
"name": "洗涤模式"
},
"water_level_set": {
"name": "水位设定"
},
@@ -313,6 +403,11 @@
}
}
},
"time": {
"timing_regeneration": {
"name": "定时再生时间"
}
},
"sensor": {
"bathing_leaving_temperature": {
"name": "淋浴出水温度"
@@ -353,6 +448,9 @@
"dehydration_time_value": {
"name": "脱水时间值"
},
"temperature": {
"name": "目标温度"
},
"wash_time_value": {
"name": "洗涤时间值"
},
@@ -362,6 +460,9 @@
"dirty_degree": {
"name": "脏度"
},
"detergent": {
"name": "洗涤剂"
},
"intensity": {
"name": "强度"
},
@@ -386,9 +487,29 @@
"energy_consumption": {
"name": "能耗"
},
"error": {
"name": "错误",
"state": {
"no_error": "无故障",
"e1_position_not_found": "E1 一直找不到工作位",
"e2_photo_sensor_no_signal": "E2 光感没有发出信号",
"e3_motor_not_running": "E3 电机不转",
"e4_wrong_position": "E4 工作位不正确",
"e1_motor_fault": "E1 电机故障",
"e5_communication_fault": "E5 通信故障",
"e6_salt_sensor_fault": "E6 盐位传感器故障",
"e7_chlorine_sterilization_fault": "E7 余氯杀菌故障"
}
},
"error_code": {
"name": "错误码"
},
"estimated_energy_consumption": {
"name": "预估能耗"
},
"estimated_water_consumption": {
"name": "预估水耗"
},
"fan_level": {
"name": "当前风扇档位"
},
@@ -452,6 +573,9 @@
"keep_warm_time": {
"name": "保温时间"
},
"left_salt": {
"name": "剩余盐量"
},
"middle_compartment_remaining": {
"name": "中层剩余时间"
},
@@ -488,6 +612,12 @@
"refrigerator_setting_temp": {
"name": "冷藏室设置温度"
},
"regeneration_left_seconds": {
"name": "再生剩余秒数"
},
"remaining_days": {
"name": "剩余天数"
},
"right_flex_zone_actual_temp": {
"name": "右变温区实际温度"
},
@@ -500,12 +630,18 @@
"rinse_level": {
"name": "漂洗档位"
},
"salt_setting": {
"name": "盐量设置"
},
"seat_temperature": {
"name": "当前座温"
},
"soak_time": {
"name": "浸泡时间"
},
"soft_available": {
"name": "软化水可用量"
},
"softener": {
"name": "柔顺剂"
},
@@ -560,17 +696,68 @@
"wash_time": {
"name": "洗涤时间档位"
},
"detergent": {
"name": "洗涤剂"
},
"water_consumption": {
"name": "总耗水量"
},
"water_consumption_average": {
"name": "平均耗水量"
},
"water_consumption_big": {
"name": "总耗水量"
},
"water_temperature": {
"name": "水温"
},
"water_level": {
"name": "水位"
},
"water_temperature": {
"name": "水温"
"wind": {
"name": ""
},
"typeinfo": {
"name": "类型信息"
},
"use_days": {
"name": "使用天数"
},
"elec_heat": {
"name": "电加热"
},
"water_pump": {
"name": "水泵"
},
"four_way": {
"name": "四通阀"
},
"back_water": {
"name": "回水"
},
"sterilize": {
"name": "杀菌"
},
"disinfection_temperature": {
"name": "消毒温度"
},
"max_temperature": {
"name": "最高目标温度"
},
"vacation_start_year": {
"name": "假期开始年"
},
"vacation_start_month": {
"name": "假期开始月"
},
"vacation_start_day": {
"name": "假期开始日"
},
"auto_sterilize_week": {
"name": "自动杀菌星期"
},
"auto_sterilize_hour": {
"name": "自动杀菌小时"
},
"auto_sterilize_minute": {
"name": "自动杀菌分钟"
},
"working_time": {
"name": "工作时间"
@@ -580,6 +767,42 @@
},
"variable_mode": {
"name": "变温区模式"
},
"compressor_frequency": {
"name": "压缩机频率"
},
"target_compressor_frequency": {
"name": "目标压缩机频率"
},
"compressor_current": {
"name": "压缩机电流"
},
"compressor_voltage": {
"name": "压缩机电压"
},
"compressor_power": {
"name": "压缩机功率"
},
"indoor_coil_temperature": {
"name": "室内盘管温度 (T1)"
},
"evaporator_temperature": {
"name": "蒸发器温度 (T2)"
},
"outdoor_ambient_temperature": {
"name": "室外环境温度 (T4)"
},
"discharge_pipe_temperature": {
"name": "排气管温度 (TP)"
},
"indoor_fan_speed": {
"name": "室内风机转速"
},
"target_indoor_fan_speed": {
"name": "目标室内风机转速"
},
"velocity": {
"name": "流速"
}
},
"switch": {
@@ -595,27 +818,39 @@
"breezeless": {
"name": "无风感"
},
"child_lock": {
"name": "童锁"
},
"cl_sterilization": {
"name": "CL 杀菌"
},
"comfort_mode": {
"name": "舒省模式"
},
"cold_water_single": {
"name": "单次零冷水"
},
"cold_water_dot": {
"name": "点动零冷水"
},
"dhw_power": {
"name": "生活热水电源开关"
},
"disinfect": {
"name": "消毒"
},
"vacation_mode": {
"name": "假期模式"
},
"dry": {
"name": "干燥"
},
"cold_water_single": {
"name": "单次零冷水"
},
"cold_water_dot": {
"name": "点动零冷水"
},
"eco_mode": {
"name": "ECO模式"
},
"memory": {
"name": "记忆"
},
"fast_dhw": {
"name": "快速生活热水"
},
@@ -626,12 +861,14 @@
"name": "防霜冻"
},
"heating_power": {
"name": "加热",
"_comments": "E2 加热功率, E6 加热开关"
"name": "加热"
},
"indirect_wind": {
"name": "防直吹"
},
"leak_water_protection": {
"name": "漏水保护"
},
"light": {
"name": "灯光"
},
@@ -662,9 +899,15 @@
"prompt_tone": {
"name": "提示音"
},
"regeneration": {
"name": "再生"
},
"water_pump": {
"name": "水泵"
},
"water_way": {
"name": "水路"
},
"screen_display": {
"name": "屏幕显示"
},
@@ -692,12 +935,18 @@
"smelly_sensor": {
"name": "异味感应"
},
"soften": {
"name": "软化功能"
},
"standby": {
"name": "待机"
},
"start": {
"name": "启动"
},
"sterilization": {
"name": "杀菌"
},
"storage": {
"name": "保管开关"
},
+15 -15
View File
@@ -151,7 +151,7 @@ class MideaWaterHeater(MideaEntity, WaterHeaterEntity):
"""Midea Water Heater target temperature."""
return cast("float", self._device.get_attribute("target_temperature"))
def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea Water Heater set temperature."""
if ATTR_TEMPERATURE not in kwargs:
return
@@ -170,23 +170,23 @@ class MideaWaterHeater(MideaEntity, WaterHeaterEntity):
return None
return cast("list", self._device.preset_modes)
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Water Heater turn on."""
self._device.set_attribute(attr="power", value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Water Heater turn off."""
self._device.set_attribute(attr="power", value=False)
async def async_turn_on(self, **kwargs: Any) -> None: # noqa: ANN401
async def async_turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea Water Heater async turn on."""
await self.hass.async_add_executor_job(ft.partial(self.turn_on, **kwargs))
async def async_turn_off(self, **kwargs: Any) -> None: # noqa: ANN401
async def async_turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea Water Heater async off."""
await self.hass.async_add_executor_job(ft.partial(self.turn_off, **kwargs))
def update_state(self, status: Any) -> None: # noqa: ANN401, ARG002
def update_state(self, status: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea Water Heater update state."""
if not self.hass:
_LOGGER.warning(
@@ -195,7 +195,7 @@ class MideaWaterHeater(MideaEntity, WaterHeaterEntity):
type(self),
)
return
self.schedule_update_ha_state()
self.schedule_update_if_running()
class MideaE2WaterHeater(MideaWaterHeater):
@@ -300,7 +300,7 @@ class MideaC3WaterHeater(MideaWaterHeater):
"""Midea C3 Water Heater target temperature."""
return cast("float", self._device.get_attribute(C3Attributes.dhw_target_temp))
def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea C3 Water Heater set temperature."""
if ATTR_TEMPERATURE not in kwargs:
return
@@ -317,11 +317,11 @@ class MideaC3WaterHeater(MideaWaterHeater):
"""Midea C3 Water Heater max temperature."""
return cast("float", self._device.get_attribute(C3Attributes.dhw_temp_max))
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea C3 Water Heater turn on."""
self._device.set_attribute(attr=C3Attributes.dhw_power, value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea C3 Water Heater turn off."""
self._device.set_attribute(attr=C3Attributes.dhw_power, value=False)
@@ -387,7 +387,7 @@ class MideaE6WaterHeater(MideaWaterHeater):
"""Midea E6 Water Heater target temperature."""
return cast("float", self._device.get_attribute(self._target_temperature_attr))
def set_temperature(self, **kwargs: Any) -> None: # noqa: ANN401
def set_temperature(self, **kwargs: Any) -> None: # ruff:ignore[any-type]
"""Midea E6 Water Heater set temperature."""
if ATTR_TEMPERATURE not in kwargs:
return
@@ -399,7 +399,7 @@ class MideaE6WaterHeater(MideaWaterHeater):
"""Midea E6 Water Heater min temperature."""
min_temperature = cast(
"list[str]",
self._device.get_attribute(E6Attributes.min_temperature),
self._device.get_attribute(E6Attributes.temperature_min),
)
return cast(
"float",
@@ -411,18 +411,18 @@ class MideaE6WaterHeater(MideaWaterHeater):
"""Midea E6 Water Heater max temperature."""
max_temperature = cast(
"list[str]",
self._device.get_attribute(E6Attributes.max_temperature),
self._device.get_attribute(E6Attributes.temperature_max),
)
return cast(
"float",
max_temperature[self._use],
)
def turn_on(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_on(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea E6 Water Heater turn on."""
self._device.set_attribute(attr=self._power_attr, value=True)
def turn_off(self, **kwargs: Any) -> None: # noqa: ANN401, ARG002
def turn_off(self, **kwargs: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Midea E6 Water Heater turn off."""
self._device.set_attribute(attr=self._power_attr, value=False)