diff --git a/README.md b/README.md index 1931bd4..76aabc1 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ If this integration helps you, you can support Smart Service: - RTK robot position as a `device_tracker`. - Optional RTK address sensor using OpenStreetMap Nominatim reverse geocoding, disabled by default. - Switches for Smart edge cutting, Save the hedgehogs and schedule edge procedure. -- Daily mowing progress, remaining progress, mowed area, lawn area and efficiency sensors when available from the API. +- Next mowing time sensor, daily and remaining progress, today and total mowed area, lawn area and mowing efficiency sensors when available from the API. - Separate smart mowing automation blueprint repository. -- Polish and English translations. +- Translations: Polish, English, French, German, Dutch, Spanish, Italian, Swedish, Norwegian and Danish, including localized entity states, schedule and calendar. - Optional raw payload entities for debugging, disabled by default. ## Installation With HACS @@ -84,7 +84,7 @@ The exact entity list depends on what your mower reports. Typical entities inclu - `calendar` mowing schedule - `camera` RTK map - `device_tracker` RTK robot position -- `sensor` battery, status, error, readiness, cloud connection, RSSI, schedule, rain delay, RTK map, RTK trail, daily progress, remaining progress, mowed area, runtime, efficiency and maintenance values +- `sensor` battery, status, error, readiness, cloud connection, RSSI, schedule, next schedule, rain delay, RTK map, RTK trail, daily progress, remaining progress, today and total mowed area, lawn area, runtime, efficiency and maintenance values - `binary_sensor` online, IoT/MQTT registration, locked, rain, party mode and pause mode - `switch` firmware auto update, mower lock, native schedule, Smart edge cutting, Save the hedgehogs and schedule edge procedure - `number` rain delay, schedule time extension, lawn area and lawn perimeter @@ -126,6 +126,14 @@ RTK maps and address lookups can contain precise garden geometry and coordinates Before opening an issue, remove private data from logs and screenshots. See [SECURITY.md](SECURITY.md). +## Mowed area + +The mower reports its mowing figures as **covered area** (the surface the blades pass over), not unique lawn area. Because a robot mows with overlapping passes, the **Today mowed area** and **Total area mowed** sensors can legitimately exceed your lawn size, and **Daily progress** reaches 100% once the covered area matches the lawn size. **Today mowed area** is derived from a local-midnight baseline and is rebuilt after a restart or a counter reset. + +## Entity naming + +The `lawn_mower` entity is the device's primary entity and has no name of its own: its displayed name is exactly the device name (e.g. just "Vision Cloud" instead of "Vision Cloud Mower"). This is both for readability and for compatibility with third-party cards such as [landroid-card](https://github.com/Barma-lej/landroid-card), which strip the device name from every other entity's label using the primary entity's name as the prefix; a redundant word there (like "Mower") previously prevented the prefix from matching. + ## Limitations The Worx / Positec cloud API is not officially public. Some endpoints used here are reverse-engineered and can change without notice. This is a best-effort custom integration, not official Worx software. diff --git a/custom_components/worx_vision_cloud/calendar.py b/custom_components/worx_vision_cloud/calendar.py index d7d3297..9fffb26 100644 --- a/custom_components/worx_vision_cloud/calendar.py +++ b/custom_components/worx_vision_cloud/calendar.py @@ -16,9 +16,25 @@ get_dict_value, schedule_day_index, schedule_day_label, + schedule_language, schedule_slots, ) +# Calendar event text is free-form and outside translations/*.json, so it is +# localized here from the UI language (falls back to English). +EVENT_SUMMARY = { + "en": "Mowing", + "de": "Mähen", + "fr": "Tonte", + "pl": "Koszenie trawnika", +} +EVENT_LABELS = { + "en": {"day": "Day", "duration": "Duration", "edge": "Edge cutting", "source": "Source", "yes": "yes"}, + "de": {"day": "Tag", "duration": "Dauer", "edge": "Kantenschnitt", "source": "Quelle", "yes": "ja"}, + "fr": {"day": "Jour", "duration": "Durée", "edge": "Coupe de bordure", "source": "Source", "yes": "oui"}, + "pl": {"day": "Dzień", "duration": "Czas trwania", "edge": "Koszenie krawędzi", "source": "Źródło", "yes": "tak"}, +} + async def async_setup_entry( hass: HomeAssistant, @@ -39,12 +55,18 @@ class WorxVisionScheduleCalendar(WorxVisionEntity, CalendarEntity): """Read-only mowing schedule calendar.""" _attr_icon = "mdi:calendar-clock" - _attr_name = "Harmonogram koszenia" + _attr_translation_key = "schedule" def __init__(self, coordinator, entry, serial_number: str) -> None: """Initialize schedule calendar.""" super().__init__(coordinator, entry, serial_number, "schedule_calendar") + @property + def _language(self) -> str: + """Return the active Home Assistant UI language.""" + config = getattr(self.hass, "config", None) + return getattr(config, "language", None) or "en" + @property def event(self) -> CalendarEvent | None: """Return the current or next scheduled mowing event.""" @@ -74,6 +96,7 @@ def _events_between( ) -> list[CalendarEvent]: """Build weekly schedule occurrences for the requested range.""" events: list[CalendarEvent] = [] + language = self._language tzinfo = start_date.tzinfo or dt_util.DEFAULT_TIME_ZONE first_day = start_date.date() - dt.timedelta(days=1) last_day = end_date.date() + dt.timedelta(days=1) @@ -85,7 +108,7 @@ def _events_between( if schedule_day_index(get_dict_value(slot, "day")) != current_day.weekday(): continue - event = _slot_to_event(slot, current_day, tzinfo) + event = _slot_to_event(slot, current_day, tzinfo, language) if event is None: continue if event.end <= start_date or event.start >= end_date: @@ -99,8 +122,9 @@ def _slot_to_event( slot: Any, event_date: dt.date, tzinfo: dt.tzinfo, + language: str = "en", ) -> CalendarEvent | None: - """Convert one schedule slot to a calendar event occurrence.""" + """Convert one schedule slot to a localized calendar event occurrence.""" start_time = _parse_time(get_dict_value(slot, "start")) if start_time is None: return None @@ -117,21 +141,23 @@ def _slot_to_event( return None end = start + dt.timedelta(minutes=duration) - day_label = schedule_day_label(get_dict_value(slot, "day")) + lang = schedule_language(language) + labels = EVENT_LABELS[lang] + day_label = schedule_day_label(get_dict_value(slot, "day"), lang) duration = _duration_minutes(slot) - description_parts = [f"Dzien: {day_label}"] + description_parts = [f"{labels['day']}: {day_label}"] if duration is not None: - description_parts.append(f"Czas trwania: {duration} min") + description_parts.append(f"{labels['duration']}: {duration} min") if get_dict_value(slot, "boundary"): - description_parts.append("Koszenie krawedzi: tak") + description_parts.append(f"{labels['edge']}: {labels['yes']}") source = get_dict_value(slot, "source") if source is not None: - description_parts.append(f"Zrodlo: {source}") + description_parts.append(f"{labels['source']}: {source}") return CalendarEvent( start=start, end=end, - summary="Koszenie trawnika", + summary=EVENT_SUMMARY[lang], description="\n".join(description_parts), ) diff --git a/custom_components/worx_vision_cloud/camera.py b/custom_components/worx_vision_cloud/camera.py index 77bf7f1..452422b 100644 --- a/custom_components/worx_vision_cloud/camera.py +++ b/custom_components/worx_vision_cloud/camera.py @@ -75,7 +75,7 @@ class WorxVisionMapCamera(WorxVisionEntity, Camera): """RTK map rendered from Worx map geometry.""" _attr_icon = "mdi:map" - _attr_name = "Mapa RTK" + _attr_translation_key = "rtk_map_camera" def __init__(self, coordinator, entry, serial_number: str) -> None: """Initialize RTK map camera.""" diff --git a/custom_components/worx_vision_cloud/coordinator.py b/custom_components/worx_vision_cloud/coordinator.py index b028e38..f8f814e 100644 --- a/custom_components/worx_vision_cloud/coordinator.py +++ b/custom_components/worx_vision_cloud/coordinator.py @@ -6,13 +6,14 @@ from datetime import UTC, datetime, timedelta import json import logging -from typing import Any +from typing import Any, Callable from aiohttp import ClientError, ClientTimeout from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from pyworxcloud import DeviceHandler, LandroidEvent, WorxCloud @@ -32,6 +33,7 @@ "(https://github.com/SmartServicePL/Worx-Vision-Cloud-PLUS)" ) PRODUCT_ITEM_CACHE_TTL = timedelta(minutes=5) +LIVE_REFRESH_INTERVAL = timedelta(minutes=5) FIRMWARE_UPGRADE_CACHE_TTL = timedelta(minutes=30) RTK_TRAIL_MAX_POINTS = 300 DEFAULT_ONE_TIME_MOWING_RUNTIME = 60 @@ -68,7 +70,13 @@ def __init__(self, hass: HomeAssistant, cloud: WorxCloud) -> None: _LOGGER, name=DOMAIN, update_interval=None, - always_update=False, + # `_device_map()` returns the same DeviceHandler instances pyworxcloud + # already holds, and `_enrich_device()` mutates them in place (e.g. the + # product-item area_mowed figure). With always_update=False the + # coordinator compares data by equality, which is always True here + # (same object references), so it silently skips notifying entities + # even when the mutated attributes actually changed. + always_update=True, ) self.cloud = cloud self._event_lock = asyncio.Lock() @@ -84,6 +92,7 @@ def __init__(self, hass: HomeAssistant, cloud: WorxCloud) -> None: str, deque[tuple[datetime, float, float]] ] = {} self._one_time_mowing_options: dict[str, dict[str, Any]] = {} + self._unsub_periodic_refresh: Callable[[], None] | None = None async def async_setup(self) -> None: """Attach pyworxcloud callbacks.""" @@ -98,10 +107,37 @@ def _on_api_update(api_data: dict[str, Any], **_: Any) -> None: self.cloud.set_callback(LandroidEvent.DATA_RECEIVED, _on_data_received) self.cloud.set_callback(LandroidEvent.API, _on_api_update) + self._unsub_periodic_refresh = async_track_time_interval( + self.hass, self._async_periodic_device_refresh, LIVE_REFRESH_INTERVAL + ) + async def async_shutdown(self) -> None: """Detach callbacks.""" self.cloud.set_callback(LandroidEvent.DATA_RECEIVED, lambda **_: None) self.cloud.set_callback(LandroidEvent.API, lambda **_: None) + if self._unsub_periodic_refresh is not None: + self._unsub_periodic_refresh() + self._unsub_periodic_refresh = None + + async def _async_periodic_device_refresh(self, _now: datetime) -> None: + """Ask each mower for a fresh update on a fixed cadence. + + Some pyworxcloud data (e.g. work-time statistics used by the daily + progress/area sensors) is only included in the mower's MQTT payload + when it responds to an explicit update request, not on every routine + push. Relying solely on push events or the sporadic LandroidEvent.API + callback can leave those figures stale for hours during active + mowing, so ask every known device to report in on this interval. + """ + for serial_number in list((self.data or {}).keys()): + try: + await self.async_request_device_update(serial_number) + except Exception: # noqa: BLE001 + _LOGGER.debug( + "Periodic refresh failed for device %s", + serial_number, + exc_info=True, + ) async def _handle_push_update(self, device: DeviceHandler) -> None: """Merge one pushed device update.""" @@ -119,10 +155,18 @@ async def _handle_push_update(self, device: DeviceHandler) -> None: async def _refresh_from_cloud_cache(self) -> dict[str, DeviceHandler]: """Return current cloud cache.""" devices = _device_map(self.cloud) - await asyncio.gather( + results = await asyncio.gather( *(self._enrich_device(serial, device) for serial, device in devices.items()), return_exceptions=True, ) + for serial, result in zip(devices, results): + if isinstance(result, Exception): + _LOGGER.warning( + "Failed to enrich device %s with REST API data (area mowed, " + "firmware, RTK map may be stale): %s", + serial, + result, + ) return devices async def _async_update_data(self) -> dict[str, DeviceHandler]: @@ -751,6 +795,15 @@ async def _enrich_device(self, serial_number: str, device: DeviceHandler) -> Non product_item = await self.async_get_product_item(serial_number) if product_item is not None: setattr(device, "_worx_vision_product_item", product_item) + _LOGGER.debug( + "Enriched device %s: area_mowed=%s", + serial_number, + product_item.get("area_mowed"), + ) + else: + _LOGGER.debug( + "No product item data returned for device %s", serial_number + ) firmware_info = await self.async_get_firmware_upgrade_info(serial_number) if firmware_info is not None: diff --git a/custom_components/worx_vision_cloud/device_tracker.py b/custom_components/worx_vision_cloud/device_tracker.py index f47a043..de4e022 100644 --- a/custom_components/worx_vision_cloud/device_tracker.py +++ b/custom_components/worx_vision_cloud/device_tracker.py @@ -3,8 +3,7 @@ from typing import Any -from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SourceType +from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -33,7 +32,7 @@ class WorxVisionLocationTracker(WorxVisionEntity, TrackerEntity): """GPS/RTK location tracker for one mower.""" _attr_icon = "mdi:map-marker-radius-outline" - _attr_name = "Pozycja RTK" + _attr_translation_key = "rtk_position" _attr_source_type = SourceType.GPS def __init__(self, coordinator, entry, serial_number: str) -> None: diff --git a/custom_components/worx_vision_cloud/helpers.py b/custom_components/worx_vision_cloud/helpers.py index e3cf90d..e374628 100644 --- a/custom_components/worx_vision_cloud/helpers.py +++ b/custom_components/worx_vision_cloud/helpers.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import date, datetime +from datetime import date, datetime, time, timedelta from enum import Enum import json from math import cos, hypot, radians @@ -10,6 +10,12 @@ from homeassistant.util import slugify +# Shared with lawn_mower.py and sensor.py so both agree on what "mowing" means +# (used e.g. to track today's actual mowing time independent of Worx's own, +# sometimes-stale work-time statistics). +MOWING_STATUS_IDS = {7, 8, 12, 32, 110, 111} +STARTING_STATUS_IDS = {2, 3, 33, 103} + RAW_SOURCE_ATTRS = ( "raw_dat", "raw_cfg", @@ -48,16 +54,42 @@ "schedules.slots.count", } +SCHEDULE_DEFAULT_LANGUAGE = "en" + +# Schedule text is free-form sensor state that Home Assistant cannot translate +# through translations/*.json, so it is localized here from the UI language. SCHEDULE_DAY_LABELS = { - "monday": "pon", - "tuesday": "wt", - "wednesday": "sr", - "thursday": "czw", - "friday": "pt", - "saturday": "sob", - "sunday": "niedz", + "en": { + "monday": "Mon", "tuesday": "Tue", "wednesday": "Wed", "thursday": "Thu", + "friday": "Fri", "saturday": "Sat", "sunday": "Sun", + }, + "de": { + "monday": "Mo", "tuesday": "Di", "wednesday": "Mi", "thursday": "Do", + "friday": "Fr", "saturday": "Sa", "sunday": "So", + }, + "fr": { + "monday": "lun", "tuesday": "mar", "wednesday": "mer", "thursday": "jeu", + "friday": "ven", "saturday": "sam", "sunday": "dim", + }, + "pl": { + "monday": "pon", "tuesday": "wt", "wednesday": "śr", "thursday": "czw", + "friday": "pt", "saturday": "sob", "sunday": "niedz", + }, +} + +SCHEDULE_TEXT_LABELS = { + "en": {"none": "no active slots", "count": "{count} active slots", "edge": "+ edge"}, + "de": {"none": "keine aktiven Zeitfenster", "count": "{count} aktive Zeitfenster", "edge": "+ Kante"}, + "fr": {"none": "aucun créneau actif", "count": "{count} créneaux actifs", "edge": "+ bordure"}, + "pl": {"none": "brak aktywnych slotów", "count": "{count} aktywnych slotów", "edge": "+ krawędź"}, } + +def schedule_language(language: Any) -> str: + """Return a supported schedule language code (falls back to English).""" + code = str(language or "").lower().split("-")[0] + return code if code in SCHEDULE_DAY_LABELS else SCHEDULE_DEFAULT_LANGUAGE + SCHEDULE_DAY_INDEX = { "monday": 0, "tuesday": 1, @@ -368,17 +400,79 @@ def schedule_day_index(day: Any) -> int | None: return SCHEDULE_DAY_INDEX.get(str(day).lower()) -def schedule_day_label(day: Any) -> str: - """Return a short human label for a schedule day.""" +def parse_schedule_time(value: Any) -> time | None: + """Parse an HH:MM schedule time from pyworxcloud data.""" + if not isinstance(value, str) or ":" not in value: + return None + hour, minute, *_ = value.split(":") + try: + return time(hour=int(hour), minute=int(minute)) + except ValueError: + return None + + +def _library_next_schedule_start(device: Any, now: datetime) -> datetime | None: + """Return the next start computed by pyworxcloud, if available. + + pyworxcloud exposes ``schedules["next_schedule_start"]`` as a wall-clock + string ("%Y-%m-%d %H:%M:%S"); the digits are the local schedule time, so we + attach ``now``'s timezone to make it timezone-aware. + """ + schedules = getattr(device, "schedules", {}) or {} + raw = get_dict_value(schedules, "next_schedule_start") + if not isinstance(raw, str) or not raw.strip(): + return None + try: + naive = datetime.strptime(raw.strip(), "%Y-%m-%d %H:%M:%S") + except ValueError: + return None + return naive.replace(tzinfo=now.tzinfo) + + +def next_schedule_start(device: Any, now: datetime) -> datetime | None: + """Return the next scheduled mowing start at or after ``now``. + + Prefers the value already computed by pyworxcloud + (``schedules["next_schedule_start"]``) and falls back to deriving it from the + weekly slots ourselves. Returns a timezone-aware datetime (matching ``now``'s + tzinfo) or None when no schedule is configured. + """ + from_library = _library_next_schedule_start(device, now) + if from_library is not None: + return from_library + + slots = schedule_slots(device) + if not slots: + return None + + candidates: list[datetime] = [] + for offset in range(0, 8): + day = (now + timedelta(days=offset)).date() + for slot in slots: + if schedule_day_index(get_dict_value(slot, "day")) != day.weekday(): + continue + start_time = parse_schedule_time(get_dict_value(slot, "start")) + if start_time is None: + continue + start = datetime.combine(day, start_time, tzinfo=now.tzinfo) + if start >= now: + candidates.append(start) + + return min(candidates) if candidates else None + + +def schedule_day_label(day: Any, language: str = SCHEDULE_DEFAULT_LANGUAGE) -> str: + """Return a short, localized human label for a schedule day.""" if day is None: return "" - day_text = str(day).lower() - return SCHEDULE_DAY_LABELS.get(day_text, str(day)) + labels = SCHEDULE_DAY_LABELS[schedule_language(language)] + return labels.get(str(day).lower(), str(day)) -def schedule_slot_summary(slot: Any) -> str: - """Return one compact schedule slot line.""" - day = schedule_day_label(get_dict_value(slot, "day")) +def schedule_slot_summary(slot: Any, language: str = SCHEDULE_DEFAULT_LANGUAGE) -> str: + """Return one compact, localized schedule slot line.""" + lang = schedule_language(language) + day = schedule_day_label(get_dict_value(slot, "day"), lang) start = get_dict_value(slot, "start") end = get_dict_value(slot, "end") duration = get_dict_value(slot, "duration_extended") @@ -393,23 +487,26 @@ def schedule_slot_summary(slot: Any) -> str: text = day or "slot" if get_dict_value(slot, "boundary"): - text = f"{text} + krawedz" + text = f"{text} {SCHEDULE_TEXT_LABELS[lang]['edge']}" return text -def schedule_summary(device: Any) -> str | None: - """Return a compact schedule summary for Home Assistant state.""" +def schedule_summary(device: Any, language: str = SCHEDULE_DEFAULT_LANGUAGE) -> str | None: + """Return a compact, localized schedule summary for Home Assistant state.""" + lang = schedule_language(language) slots = schedule_slots(device) if not slots: - return "brak aktywnych slotow" + return SCHEDULE_TEXT_LABELS[lang]["none"] - summary = ", ".join(schedule_slot_summary(slot) for slot in slots) + summary = ", ".join(schedule_slot_summary(slot, lang) for slot in slots) if len(summary) <= MAX_STRING_STATE_LENGTH: return summary - return f"{len(slots)} aktywnych slotow" + return SCHEDULE_TEXT_LABELS[lang]["count"].format(count=len(slots)) -def schedule_attributes(device: Any) -> dict[str, Any]: +def schedule_attributes( + device: Any, language: str = SCHEDULE_DEFAULT_LANGUAGE +) -> dict[str, Any]: """Return structured schedule data for cards and templates.""" schedules = getattr(device, "schedules", {}) or {} slots = schedule_slots(device) @@ -420,7 +517,7 @@ def schedule_attributes(device: Any) -> dict[str, Any]: "slots": [ { "day": get_dict_value(slot, "day"), - "day_label": schedule_day_label(get_dict_value(slot, "day")), + "day_label": schedule_day_label(get_dict_value(slot, "day"), language), "start": get_dict_value(slot, "start"), "end": get_dict_value(slot, "end"), "duration": get_dict_value(slot, "duration"), diff --git a/custom_components/worx_vision_cloud/lawn_mower.py b/custom_components/worx_vision_cloud/lawn_mower.py index 323871d..1a4e2f2 100644 --- a/custom_components/worx_vision_cloud/lawn_mower.py +++ b/custom_components/worx_vision_cloud/lawn_mower.py @@ -14,13 +14,17 @@ from .const import DOMAIN from .entity import WorxVisionEntity -from .helpers import get_dict_value, rtk_at_station, rtk_distance_to_station_m +from .helpers import ( + MOWING_STATUS_IDS, + STARTING_STATUS_IDS, + get_dict_value, + rtk_at_station, + rtk_distance_to_station_m, +) _LOGGER = logging.getLogger(__name__) -MOWING_STATUS_IDS = {7, 8, 12, 32, 110, 111} RETURNING_STATUS_IDS = {4, 5, 6, 30, 104} -STARTING_STATUS_IDS = {2, 3, 33, 103} PAUSED_STATUS_IDS = {34} DOCKED_STATUS_IDS = {1} ERROR_STATUS_IDS = {9, 10, 13} @@ -70,6 +74,12 @@ async def async_setup_entry( class WorxVisionLawnMower(WorxVisionEntity, LawnMowerEntity): """Worx Landroid mower entity.""" + # This is the device's primary/main entity, so it has no name of its own + # (HA convention): with has_entity_name=True its friendly_name becomes + # exactly the device name, which is what lets companion cards like + # landroid-card correctly strip the device name from other entities. + _attr_name = None + _attr_supported_features = ( LawnMowerEntityFeature.START_MOWING | LawnMowerEntityFeature.PAUSE @@ -79,7 +89,6 @@ class WorxVisionLawnMower(WorxVisionEntity, LawnMowerEntity): def __init__(self, coordinator, entry, serial_number: str) -> None: """Initialize mower.""" super().__init__(coordinator, entry, serial_number, "mower") - self._attr_translation_key = "mower" @property def available(self) -> bool: diff --git a/custom_components/worx_vision_cloud/select.py b/custom_components/worx_vision_cloud/select.py index a88d202..213a1c5 100644 --- a/custom_components/worx_vision_cloud/select.py +++ b/custom_components/worx_vision_cloud/select.py @@ -14,9 +14,36 @@ from .entity import WorxVisionEntity from .helpers import get_dict_value, rtk_map_attributes -ALL_ZONES_OPTION = "Wszystkie strefy" +DEFAULT_LANGUAGE = "en" MAX_COMBINATION_ZONES = 5 +# The select options are built from dynamic RTK zone combinations, so they cannot be +# declared in translations/*.json. They are localized here from the HA UI language; +# unknown languages fall back to English. Polish wording is preserved. +ALL_ZONES_LABELS = { + "en": "All zones", + "fr": "Toutes les zones", + "de": "Alle Zonen", + "pl": "Wszystkie strefy", +} +ZONE_SINGULAR_LABELS = { + "en": "Zone", + "fr": "Zone", + "de": "Zone", + "pl": "Strefa", +} +ZONE_PLURAL_LABELS = { + "en": "Zones", + "fr": "Zones", + "de": "Zonen", + "pl": "Strefy", +} + + +def _all_zones_label(language: str) -> str: + """Return the localized 'all zones' option label.""" + return ALL_ZONES_LABELS.get(language, ALL_ZONES_LABELS[DEFAULT_LANGUAGE]) + async def async_setup_entry( hass: HomeAssistant, @@ -48,26 +75,28 @@ def _zone_ids(device: Any) -> list[int]: return sorted(zone_ids) -def _option_label(zone_ids: list[int]) -> str: +def _option_label(zone_ids: list[int], language: str = DEFAULT_LANGUAGE) -> str: """Return a user-facing label for one zone selection.""" if not zone_ids: - return ALL_ZONES_OPTION + return _all_zones_label(language) if len(zone_ids) == 1: - return f"Strefa {zone_ids[0]}" - return "Strefy " + ", ".join(str(zone_id) for zone_id in zone_ids) + singular = ZONE_SINGULAR_LABELS.get(language, ZONE_SINGULAR_LABELS[DEFAULT_LANGUAGE]) + return f"{singular} {zone_ids[0]}" + plural = ZONE_PLURAL_LABELS.get(language, ZONE_PLURAL_LABELS[DEFAULT_LANGUAGE]) + return plural + " " + ", ".join(str(zone_id) for zone_id in zone_ids) -def _option_map(zone_ids: list[int]) -> dict[str, list[int]]: +def _option_map(zone_ids: list[int], language: str = DEFAULT_LANGUAGE) -> dict[str, list[int]]: """Return select option label to zone ID list mapping.""" - result: dict[str, list[int]] = {ALL_ZONES_OPTION: []} + result: dict[str, list[int]] = {_all_zones_label(language): []} if len(zone_ids) <= MAX_COMBINATION_ZONES: for count in range(1, len(zone_ids) + 1): for combo in combinations(zone_ids, count): selected = list(combo) - result[_option_label(selected)] = selected + result[_option_label(selected, language)] = selected else: for zone_id in zone_ids: - result[_option_label([zone_id])] = [zone_id] + result[_option_label([zone_id], language)] = [zone_id] return result @@ -81,12 +110,20 @@ def __init__(self, coordinator, entry, serial_number: str) -> None: """Initialize one-time mowing zones select.""" super().__init__(coordinator, entry, serial_number, "one_time_mowing_zones") + @property + def _language(self) -> str: + """Return the active Home Assistant UI language.""" + hass = getattr(self, "hass", None) + config = getattr(hass, "config", None) + return getattr(config, "language", None) or DEFAULT_LANGUAGE + @property def options(self) -> list[str]: """Return available zone choices.""" - options = _option_map(_zone_ids(self.device)) + language = self._language + options = _option_map(_zone_ids(self.device), language) current_label = _option_label( - self.coordinator.one_time_mowing_zones(self._serial_number) + self.coordinator.one_time_mowing_zones(self._serial_number), language ) if current_label not in options: options[current_label] = self.coordinator.one_time_mowing_zones( @@ -97,7 +134,9 @@ def options(self) -> list[str]: @property def current_option(self) -> str | None: """Return selected zone choice.""" - return _option_label(self.coordinator.one_time_mowing_zones(self._serial_number)) + return _option_label( + self.coordinator.one_time_mowing_zones(self._serial_number), self._language + ) @property def extra_state_attributes(self) -> dict[str, Any]: @@ -111,9 +150,10 @@ def extra_state_attributes(self) -> dict[str, Any]: async def async_select_option(self, option: str) -> None: """Select one zone choice.""" - options = _option_map(_zone_ids(self.device)) + language = self._language + options = _option_map(_zone_ids(self.device), language) current_zones = self.coordinator.one_time_mowing_zones(self._serial_number) - current_label = _option_label(current_zones) + current_label = _option_label(current_zones, language) if current_label not in options: options[current_label] = current_zones if option not in options: diff --git a/custom_components/worx_vision_cloud/sensor.py b/custom_components/worx_vision_cloud/sensor.py index 312fff5..26da02a 100644 --- a/custom_components/worx_vision_cloud/sensor.py +++ b/custom_components/worx_vision_cloud/sensor.py @@ -3,10 +3,11 @@ from asyncio import Task from dataclasses import dataclass -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from typing import Any, Callable from homeassistant.components.sensor import ( + RestoreSensor, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -25,6 +26,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util import dt as dt_util from .const import ( ATTR_RAW_PATH, @@ -36,7 +38,10 @@ from .entity import WorxVisionEntity from .helpers import ( MAX_STRING_STATE_LENGTH, + MOWING_STATUS_IDS, + STARTING_STATUS_IDS, get_dict_value, + next_schedule_start, raw_entity_path_map, raw_entity_values, raw_path_enabled_default, @@ -58,47 +63,71 @@ class WorxSensorDescription(SensorEntityDescription): attrs_fn: Callable[[Any], dict[str, Any] | None] | None = None -STATUS_LABELS_PL = { - "home": "w bazie", - "leaving home": "wyjazd z bazy", - "going home": "powrót do bazy", - "mowing": "koszenie", - "cutting edge": "przycinanie krawędzi", - "edge cutting": "przycinanie krawędzi", - "border cut": "przycinanie krawędzi", - "charging": "ładowanie", - "paused": "pauza", - "pause": "pauza", - "idle": "bezczynna", - "manual stop": "zatrzymana ręcznie", - "rain delay": "opóźnienie po deszczu", - "rain_delay": "opóźnienie po deszczu", - "locked": "zablokowana", - "error": "błąd", - "no error": "brak błędu", +# Map the raw descriptions reported by Worx to canonical, language-neutral state +# keys. The human-readable labels live in translations/*.json so Home Assistant can +# localize them per user (en/pl/fr/...), instead of being hard-coded here. +STATUS_STATE_KEYS = { + "home": "home", + "leaving home": "leaving_home", + "going home": "going_home", + "mowing": "mowing", + "cutting edge": "edge_cutting", + "edge cutting": "edge_cutting", + "border cut": "edge_cutting", + "charging": "charging", + "paused": "paused", + "pause": "paused", + "idle": "idle", + "manual stop": "manual_stop", + "rain delay": "rain_delay", + "rain_delay": "rain_delay", + "locked": "locked", + "error": "error", + "no error": "no_error", "offline": "offline", } -READINESS_LABELS_PL = { - "ready": "gotowa", - "mowing": "koszenie", - "charging": "ładowanie", - "battery_low": "niski poziom baterii", - "rain_delay": "opóźnienie po deszczu", - "error": "błąd", - "locked": "zablokowana", - "offline": "offline", -} +# Canonical option lists exposed as enum sensor states. +STATUS_STATE_OPTIONS = [ + "home", + "leaving_home", + "going_home", + "mowing", + "edge_cutting", + "charging", + "paused", + "idle", + "manual_stop", + "rain_delay", + "locked", + "error", + "no_error", + "offline", +] + +READINESS_STATE_OPTIONS = [ + "ready", + "mowing", + "charging", + "battery_low", + "rain_delay", + "error", + "locked", + "offline", +] + +CLOUD_CONNECTION_OPTIONS = ["ok", "check", "offline"] + +MAINTENANCE_STATE_OPTIONS = ["ok", "blade_service_due", "battery_service_due"] RAIN_DELAY_ERROR_DESCRIPTIONS = {"rain delay", "rain_delay"} -def _label_pl(value: Any, labels: dict[str, str]) -> str | None: - """Return a Polish label for a known Worx state.""" +def _state_key(value: Any, mapping: dict[str, str]) -> str | None: + """Map a raw Worx description to a canonical, translatable state key.""" if value is None: return None - text = str(value) - return labels.get(text.strip().lower(), text) + return mapping.get(str(value).strip().lower()) def _battery(device, key, default=None): @@ -123,8 +152,8 @@ def _status(device, key, default=None): def _status_state(device) -> str | None: if _is_rain_delay(device): - return _label_pl("rain_delay", READINESS_LABELS_PL) - return _label_pl(_status(device, "description"), STATUS_LABELS_PL) + return "rain_delay" + return _state_key(_status(device, "description"), STATUS_STATE_KEYS) def _error(device, key, default=None): @@ -132,7 +161,9 @@ def _error(device, key, default=None): def _error_state(device) -> str | None: - return _label_pl(_error(device, "description"), STATUS_LABELS_PL) + # Unmapped/rare device error descriptions surface via the raw_description + # attribute; the enum state stays None to avoid noisy non-option warnings. + return _state_key(_error(device, "description"), STATUS_STATE_KEYS) def _is_rain_delay(device) -> bool: @@ -206,7 +237,8 @@ def _first_map_zone(device): return {} -def _area_mowed_today(device): +def _area_mowed_total(device): + """Return the lifetime total mowed area reported by the mower (m²).""" value = _product_item(device, "area_mowed") try: return round(float(value), 2) @@ -260,8 +292,24 @@ def _since_reset(device, total_key: str, reset_key: str) -> int | None: return max(0, total - reset) +def _work_time_total_minutes(device) -> float | None: + """Return the lifetime mower work time in minutes. + + Prefers the MQTT-pushed statistics value (updates live while mowing) and + falls back to the REST product-item field when statistics are unavailable. + """ + value = _statistics(device, "worktime_total") + if value is None: + value = _product_item(device, "mower_work_time") + return _as_float(value) + + def _mowing_efficiency(device) -> float | None: - area = _area_mowed_today(device) + # Deliberately pairs area with the REST-only work_time (not + # _work_time_total_minutes' live-preferring value): both figures come from + # the same product-item snapshot, so the ratio stays internally consistent + # even though it only refreshes as often as that REST endpoint does. + area = _area_mowed_total(device) work_minutes = _as_float(_product_item(device, "mower_work_time")) if area is None or work_minutes in (None, 0): return None @@ -394,7 +442,7 @@ def _mowing_readiness_code(device) -> str | None: def _mowing_readiness_state(device) -> str | None: - return _label_pl(_mowing_readiness_code(device), READINESS_LABELS_PL) + return _mowing_readiness_code(device) def _mowing_readiness_attributes(device) -> dict[str, Any]: @@ -472,21 +520,6 @@ def _lawn_area(device): return round(area, 2) if area > 0 else None -def _daily_progress(device): - area_mowed = _area_mowed_today(device) - lawn_area = _lawn_area(device) - if area_mowed is None or lawn_area in (None, 0): - return None - return round(max(0, min(100, area_mowed / lawn_area * 100)), 1) - - -def _remaining_progress(device): - progress = _daily_progress(device) - if progress is None: - return None - return round(max(0, 100 - progress), 1) - - def _first_address_text(address: dict[str, Any], *keys: str) -> str | None: """Return the first non-empty text value from an address dict.""" for key in keys: @@ -587,6 +620,8 @@ def _rtk_address_attributes( key="status", translation_key="status", icon="mdi:robot-mower", + device_class=SensorDeviceClass.ENUM, + options=STATUS_STATE_OPTIONS, value_fn=_status_state, attrs_fn=_status_attributes, ), @@ -595,6 +630,8 @@ def _rtk_address_attributes( translation_key="error", icon="mdi:alert-circle-outline", entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=STATUS_STATE_OPTIONS, value_fn=_error_state, attrs_fn=lambda d: { "id": _error(d, "id"), @@ -622,17 +659,12 @@ def _rtk_address_attributes( "starting_point": _zone(d, "starting_point"), }, ), - WorxSensorDescription( - key="schedule", - translation_key="schedule", - icon="mdi:calendar-clock", - value_fn=schedule_summary, - attrs_fn=schedule_attributes, - ), WorxSensorDescription( key="mowing_readiness", translation_key="mowing_readiness", icon="mdi:clipboard-check-outline", + device_class=SensorDeviceClass.ENUM, + options=READINESS_STATE_OPTIONS, value_fn=_mowing_readiness_state, attrs_fn=_mowing_readiness_attributes, ), @@ -641,6 +673,8 @@ def _rtk_address_attributes( translation_key="cloud_connection", icon="mdi:cloud-check-outline", entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=CLOUD_CONNECTION_OPTIONS, value_fn=_cloud_connection_state, attrs_fn=_cloud_connection_attributes, ), @@ -666,38 +700,14 @@ def _rtk_address_attributes( }, ), WorxSensorDescription( - key="daily_progress", - translation_key="daily_progress", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:progress-check", - value_fn=_daily_progress, - attrs_fn=lambda d: { - "area_mowed": _area_mowed_today(d), - "lawn_area": _lawn_area(d), - }, - ), - WorxSensorDescription( - key="remaining_progress", - translation_key="remaining_progress", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:progress-clock", - value_fn=_remaining_progress, - attrs_fn=lambda d: { - "daily_progress": _daily_progress(d), - "area_mowed": _area_mowed_today(d), - "lawn_area": _lawn_area(d), - }, - ), - WorxSensorDescription( - key="area_mowed_today", - translation_key="area_mowed_today", + key="area_mowed_total", + translation_key="area_mowed_total", native_unit_of_measurement=UnitOfArea.SQUARE_METERS, device_class=SensorDeviceClass.AREA, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:grass", - value_fn=_area_mowed_today, + value_fn=_area_mowed_total, + attrs_fn=lambda d: {"lawn_area": _lawn_area(d)}, ), WorxSensorDescription( key="lawn_area", @@ -717,7 +727,7 @@ def _rtk_address_attributes( icon="mdi:speedometer", value_fn=_mowing_efficiency, attrs_fn=lambda d: { - "area_mowed": _area_mowed_today(d), + "area_mowed": _area_mowed_total(d), "mower_work_time": _product_item(d, "mower_work_time"), }, ), @@ -834,8 +844,7 @@ def _rtk_address_attributes( device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda d: _statistics(d, "worktime_total") - or _product_item(d, "mower_work_time"), + value_fn=_work_time_total_minutes, ), WorxSensorDescription( key="mower_home_time_total", @@ -872,6 +881,8 @@ def _rtk_address_attributes( translation_key="maintenance_status", icon="mdi:wrench-clock", entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=MAINTENANCE_STATE_OPTIONS, value_fn=_maintenance_state, attrs_fn=_maintenance_attributes, ), @@ -902,14 +913,6 @@ def _rtk_address_attributes( icon="mdi:axis-z-rotate-clockwise", value_fn=lambda d: _orientation(d, "yaw"), ), - WorxSensorDescription( - key="last_update", - translation_key="last_update", - device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-check", - entity_category=EntityCategory.DIAGNOSTIC, - value_fn=_last_update, - ), WorxSensorDescription( key="last_update_age", translation_key="last_update_age", @@ -941,6 +944,14 @@ async def async_setup_entry( for description in STANDARD_SENSORS ) entities.append(WorxVisionAddressSensor(coordinator, entry, serial_number)) + entities.append(WorxScheduleSensor(coordinator, entry, serial_number)) + entities.append(WorxNextScheduleSensor(coordinator, entry, serial_number)) + entities.append(WorxLastUpdateSensor(coordinator, entry, serial_number)) + entities.append(WorxAreaMowedTodaySensor(coordinator, entry, serial_number)) + entities.append(WorxDailyProgressSensor(coordinator, entry, serial_number)) + entities.append(WorxRemainingProgressSensor(coordinator, entry, serial_number)) + entities.append(WorxEstimatedAreaTodaySensor(coordinator, entry, serial_number)) + entities.append(WorxEstimatedDailyProgressSensor(coordinator, entry, serial_number)) def add_raw_entities() -> None: raw_entities: list[SensorEntity] = [] @@ -1004,6 +1015,391 @@ def extra_state_attributes(self) -> dict[str, Any] | None: return {key: value for key, value in (attrs or {}).items() if value is not None} +class WorxNextScheduleSensor(WorxVisionEntity, SensorEntity): + """Timestamp of the next scheduled mowing start.""" + + _attr_translation_key = "next_schedule" + _attr_icon = "mdi:calendar-arrow-right" + _attr_device_class = SensorDeviceClass.TIMESTAMP + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize the next schedule sensor.""" + super().__init__(coordinator, entry, serial_number, "next_schedule") + + @property + def native_value(self) -> datetime | None: + """Return the next scheduled mowing start.""" + return next_schedule_start(self.device, dt_util.now()) + + +LAST_UPDATE_REPORT_INTERVAL = timedelta(hours=24) + + +class WorxLastUpdateSensor(WorxVisionEntity, RestoreSensor): + """Timestamp of the last data received from the mower. + + The underlying value changes on every push (as often as every ~20 + seconds), which would make Home Assistant's logbook narrate a "changed" + entry that often. Since this sensor is meant as an occasional heartbeat + check rather than a live clock, it only accepts a new value once per + LAST_UPDATE_REPORT_INTERVAL and otherwise keeps reporting the previous + one, so the logbook only sees one real change per interval. + """ + + _attr_translation_key = "last_update" + _attr_icon = "mdi:clock-check" + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize the last update sensor.""" + super().__init__(coordinator, entry, serial_number, "last_update") + self._reported_value: datetime | None = None + self._reported_at: datetime | None = None + + async def async_added_to_hass(self) -> None: + """Restore the last reported value and when it was accepted.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state is None: + return + self._reported_value = _as_datetime(last_state.state) + self._reported_at = _as_datetime(last_state.attributes.get("reported_at")) + + @property + def native_value(self) -> datetime | None: + """Return the last-reported update time, refreshed at most once a day.""" + current = _last_update(self.device) + now = dt_util.utcnow() + if ( + self._reported_value is None + or self._reported_at is None + or now - self._reported_at >= LAST_UPDATE_REPORT_INTERVAL + ): + self._reported_value = current + self._reported_at = now + return self._reported_value + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Persist when the reported value was accepted, for restarts.""" + return { + "reported_at": self._reported_at.isoformat() if self._reported_at else None, + } + + +class WorxScheduleSensor(WorxVisionEntity, SensorEntity): + """Compact weekly schedule summary, localized to the UI language.""" + + _attr_translation_key = "schedule" + _attr_icon = "mdi:calendar-clock" + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize the schedule sensor.""" + super().__init__(coordinator, entry, serial_number, "schedule") + + @property + def _language(self) -> str: + """Return the active Home Assistant UI language.""" + config = getattr(self.hass, "config", None) + return getattr(config, "language", None) or "en" + + @property + def native_value(self) -> str | None: + """Return the localized schedule summary.""" + return schedule_summary(self.device, self._language) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return structured schedule data.""" + attrs = schedule_attributes(self.device, self._language) + return {key: value for key, value in attrs.items() if value is not None} + + +class _WorxDailyMowedBase(WorxVisionEntity, RestoreSensor): + """Base for sensors derived from the area mowed since local midnight. + + The mower only reports a lifetime total area, so the daily value is the + difference from a baseline captured at the start of each day. The baseline + is persisted as state attributes so it survives Home Assistant restarts. + """ + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, coordinator, entry, serial_number: str, key: str) -> None: + """Initialize the daily base sensor.""" + super().__init__(coordinator, entry, serial_number, key) + self._baseline_total: float | None = None + self._baseline_date: str | None = None + + async def async_added_to_hass(self) -> None: + """Restore the saved daily baseline.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state is None: + return + try: + self._baseline_total = float(last_state.attributes["baseline_total"]) + except (KeyError, TypeError, ValueError): + self._baseline_total = None + self._baseline_date = last_state.attributes.get("baseline_date") + + def _today_area(self) -> float | None: + """Return the area mowed since local midnight (m²).""" + total = _area_mowed_total(self.device) + if total is None: + return None + today = dt_util.now().date().isoformat() + if ( + self._baseline_total is None + or self._baseline_date != today + or total < self._baseline_total + ): + self._baseline_total = total + self._baseline_date = today + return round(max(0.0, total - self._baseline_total), 2) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Persist the daily baseline so it survives restarts.""" + return { + "baseline_total": self._baseline_total, + "baseline_date": self._baseline_date, + } + + +class WorxAreaMowedTodaySensor(_WorxDailyMowedBase): + """Area mowed since local midnight.""" + + _attr_translation_key = "area_mowed_today" + _attr_device_class = SensorDeviceClass.AREA + _attr_native_unit_of_measurement = UnitOfArea.SQUARE_METERS + _attr_icon = "mdi:grass" + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize area mowed today.""" + super().__init__(coordinator, entry, serial_number, "area_mowed_today") + + @property + def native_value(self) -> float | None: + """Return today's mowed area.""" + return self._today_area() + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the baseline plus reference figures.""" + return { + **super().extra_state_attributes, + "area_mowed_total": _area_mowed_total(self.device), + "lawn_area": _lawn_area(self.device), + } + + +class WorxDailyProgressSensor(_WorxDailyMowedBase): + """Percentage of the lawn mowed today.""" + + _attr_translation_key = "daily_progress" + _attr_native_unit_of_measurement = PERCENTAGE + _attr_icon = "mdi:progress-check" + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize daily progress.""" + super().__init__(coordinator, entry, serial_number, "daily_progress") + + @property + def native_value(self) -> float | None: + """Return today's progress in percent.""" + today = self._today_area() + lawn_area = _lawn_area(self.device) + if today is None or lawn_area in (None, 0): + return None + return round(max(0, min(100, today / lawn_area * 100)), 1) + + +class WorxRemainingProgressSensor(_WorxDailyMowedBase): + """Percentage of the lawn still to mow today.""" + + _attr_translation_key = "remaining_progress" + _attr_native_unit_of_measurement = PERCENTAGE + _attr_icon = "mdi:progress-clock" + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize remaining progress.""" + super().__init__(coordinator, entry, serial_number, "remaining_progress") + + @property + def native_value(self) -> float | None: + """Return the remaining lawn percentage for today.""" + today = self._today_area() + lawn_area = _lawn_area(self.device) + if today is None or lawn_area in (None, 0): + return None + progress = max(0, min(100, today / lawn_area * 100)) + return round(max(0, 100 - progress), 1) + + +def _is_mowing_now(device) -> bool: + """Return whether the mower is currently mowing or starting to mow. + + Mirrors lawn_mower.py's own definition of the MOWING activity, so this + always agrees with what the lawn_mower entity itself shows. + """ + status_id = get_dict_value(getattr(device, "status", {}), "id", -1) + return status_id in MOWING_STATUS_IDS or status_id in STARTING_STATUS_IDS + + +class _WorxDailyMowingTimeBase(WorxVisionEntity, RestoreSensor): + """Base for sensors derived from actual mowing time since local midnight. + + Worx's own work-time statistics (device.statistics / product-item) are + only included in some MQTT payloads and can go stale for hours during + active mowing (confirmed live: unchanged for 20+ minutes of continuous + mowing despite periodic forced refreshes). Instead, this tracks wall-clock + time spent in the mowing/starting status ourselves, from every coordinator + update, independent of whether Worx reports fresh statistics. + """ + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, coordinator, entry, serial_number: str, key: str) -> None: + """Initialize the daily mowing-time base sensor.""" + super().__init__(coordinator, entry, serial_number, key) + self._accumulated_seconds: float = 0.0 + self._streak_started_at: datetime | None = None + self._baseline_date: str | None = None + + async def async_added_to_hass(self) -> None: + """Restore the saved daily accumulator.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state is None: + return + try: + self._accumulated_seconds = float( + last_state.attributes["accumulated_seconds"] + ) + except (KeyError, TypeError, ValueError): + self._accumulated_seconds = 0.0 + self._streak_started_at = _as_datetime( + last_state.attributes.get("streak_started_at") + ) + self._baseline_date = last_state.attributes.get("baseline_date") + + def _today_mowing_minutes(self) -> float: + """Return actual mowing time since local midnight (minutes).""" + now = dt_util.utcnow() + today = dt_util.now().date().isoformat() + mowing_now = _is_mowing_now(self.device) + + if self._baseline_date != today: + # New day: drop yesterday's accumulator. A streak already running + # across midnight restarts from now rather than trying to split it. + self._accumulated_seconds = 0.0 + self._baseline_date = today + self._streak_started_at = now if mowing_now else None + + if mowing_now and self._streak_started_at is None: + self._streak_started_at = now + elif not mowing_now and self._streak_started_at is not None: + self._accumulated_seconds += (now - self._streak_started_at).total_seconds() + self._streak_started_at = None + + total_seconds = self._accumulated_seconds + if mowing_now and self._streak_started_at is not None: + total_seconds += (now - self._streak_started_at).total_seconds() + return round(max(0.0, total_seconds) / 60, 2) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Persist the accumulator so it survives restarts.""" + return { + "accumulated_seconds": self._accumulated_seconds, + "streak_started_at": self._streak_started_at.isoformat() + if self._streak_started_at + else None, + "baseline_date": self._baseline_date, + } + + +class WorxEstimatedAreaTodaySensor(_WorxDailyMowingTimeBase): + """Estimated area mowed today, from today's mowing time and average efficiency. + + area_mowed only refreshes when Worx's REST product-item endpoint reports a + new figure, which can lag for hours during active mowing. This sensor + estimates today's coverage instead as time actually spent mowing today + (tracked locally, independent of Worx's own statistics reporting) + multiplied by the mower's average mowing efficiency (m2/h, itself + REST-sourced but changes slowly), so it moves during the day even when + Total/Today area mowed are stuck waiting for Worx to recompute the real + figure. + """ + + _attr_translation_key = "estimated_area_mowed_today" + _attr_device_class = SensorDeviceClass.AREA + _attr_native_unit_of_measurement = UnitOfArea.SQUARE_METERS + _attr_icon = "mdi:grass" + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize estimated area mowed today.""" + super().__init__(coordinator, entry, serial_number, "estimated_area_mowed_today") + + @property + def native_value(self) -> float | None: + """Return today's estimated mowed area.""" + mowing_minutes = self._today_mowing_minutes() + efficiency = _mowing_efficiency(self.device) + if efficiency is None: + return None + return round(mowing_minutes / 60 * efficiency, 2) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the accumulator plus the figures used for the estimate.""" + return { + **super().extra_state_attributes, + "mowing_minutes_today": self._today_mowing_minutes(), + "mowing_efficiency": _mowing_efficiency(self.device), + } + + +class WorxEstimatedDailyProgressSensor(_WorxDailyMowingTimeBase): + """Estimated percentage of the lawn mowed today. + + Same estimate as WorxEstimatedAreaTodaySensor (today's mowing time x + average efficiency), expressed as a percentage of the known lawn area, so + it moves during the day even when the REST-based daily progress is stuck. + """ + + _attr_translation_key = "estimated_daily_progress" + _attr_native_unit_of_measurement = PERCENTAGE + _attr_icon = "mdi:progress-check" + + def __init__(self, coordinator, entry, serial_number: str) -> None: + """Initialize estimated daily progress.""" + super().__init__(coordinator, entry, serial_number, "estimated_daily_progress") + + @property + def native_value(self) -> float | None: + """Return today's estimated progress in percent.""" + mowing_minutes = self._today_mowing_minutes() + efficiency = _mowing_efficiency(self.device) + lawn_area = _lawn_area(self.device) + if efficiency is None or lawn_area in (None, 0): + return None + estimated_area = mowing_minutes / 60 * efficiency + return round(max(0, min(100, estimated_area / lawn_area * 100)), 1) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the accumulator plus the figures used for the estimate.""" + return { + **super().extra_state_attributes, + "mowing_minutes_today": self._today_mowing_minutes(), + "mowing_efficiency": _mowing_efficiency(self.device), + "lawn_area": _lawn_area(self.device), + } + + class WorxVisionAddressSensor(WorxVisionEntity, SensorEntity): """Reverse-geocoded RTK address sensor.""" diff --git a/custom_components/worx_vision_cloud/translations/da.json b/custom_components/worx_vision_cloud/translations/da.json new file mode 100644 index 0000000..bc10920 --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/da.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Brug samme login som i Worx Landroid-appen.", + "data": { + "email": "E-mail", + "password": "Adgangskode", + "cloud": "Sky", + "verify_ssl": "Bekræft SSL", + "expose_raw_entities": "Vis alle rå datafelter som enheder" + } + } + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til Worx Cloud.", + "invalid_auth": "Ugyldigt brugernavn eller adgangskode.", + "rate_limited": "Worx Cloud-anmodningsgrænse nået. Prøv igen senere." + }, + "abort": { + "already_configured": "Denne konto er allerede konfigureret." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Batteri" + }, + "status": { + "name": "Status", + "state": { + "home": "Ved basestation", + "leaving_home": "Forlader basestation", + "going_home": "På vej til basestation", + "mowing": "Slår græs", + "edge_cutting": "Kantklipning", + "charging": "Oplader", + "paused": "Sat på pause", + "idle": "Inaktiv", + "manual_stop": "Manuelt stoppet", + "rain_delay": "Regnforsinkelse", + "locked": "Låst", + "error": "Fejl", + "no_error": "Ingen fejl", + "offline": "Offline" + } + }, + "error": { + "name": "Fejl", + "state": { + "home": "Ved basestation", + "leaving_home": "Forlader basestation", + "going_home": "På vej til basestation", + "mowing": "Slår græs", + "edge_cutting": "Kantklipning", + "charging": "Oplader", + "paused": "Sat på pause", + "idle": "Inaktiv", + "manual_stop": "Manuelt stoppet", + "rain_delay": "Regnforsinkelse", + "locked": "Låst", + "error": "Fejl", + "no_error": "Ingen fejl", + "offline": "Offline" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Nuværende zone" + }, + "schedule": { + "name": "Tidsplan" + }, + "mowing_readiness": { + "name": "Klippeparathed", + "state": { + "ready": "Klar", + "mowing": "Slår græs", + "charging": "Oplader", + "battery_low": "Lavt batteriniveau", + "rain_delay": "Regnforsinkelse", + "error": "Fejl", + "locked": "Låst", + "offline": "Offline" + } + }, + "cloud_connection": { + "name": "Skyforbindelse", + "state": { + "ok": "Forbundet", + "check": "Kontrollerer", + "offline": "Offline" + } + }, + "api_capabilities": { + "name": "API-funktioner" + }, + "push_notifications": { + "name": "Push-notifikationer" + }, + "daily_progress": { + "name": "Dagens fremskridt" + }, + "remaining_progress": { + "name": "Resterende at slå" + }, + "area_mowed_today": { + "name": "Areal slået i dag" + }, + "next_schedule": { + "name": "Næste slåning" + }, + "area_mowed_total": { + "name": "Samlet slået areal" + }, + "estimated_area_mowed_today": { + "name": "Estimeret slået areal i dag" + }, + "estimated_daily_progress": { + "name": "Estimeret dagligt fremskridt" + }, + "lawn_area": { + "name": "Plænens areal" + }, + "mowing_efficiency": { + "name": "Klippeeffektivitet" + }, + "rtk_map": { + "name": "RTK-kort" + }, + "rtk_trail_points": { + "name": "RTK-sporpunkter" + }, + "rtk_address": { + "name": "RTK-adresse" + }, + "rain_delay": { + "name": "Regnforsinkelse" + }, + "rain_remaining": { + "name": "Resterende regnforsinkelse" + }, + "battery_voltage": { + "name": "Batterispænding" + }, + "battery_temperature": { + "name": "Batteritemperatur" + }, + "battery_cycles_total": { + "name": "Battericyklusser (i alt)" + }, + "battery_cycles_since_reset": { + "name": "Battericyklusser siden nulstilling" + }, + "battery_cycles_reset_at": { + "name": "Seneste nulstilling af battericyklusser" + }, + "blade_runtime_total": { + "name": "Knivenes driftstid (i alt)" + }, + "blade_runtime_current": { + "name": "Knivenes driftstid (aktuel)" + }, + "blade_runtime_reset_at": { + "name": "Seneste nulstilling af knivtid" + }, + "mower_runtime_total": { + "name": "Samlet driftstid" + }, + "mower_home_time_total": { + "name": "Samlet tid ved basestation" + }, + "mower_charging_time_total": { + "name": "Samlet opladningstid" + }, + "mower_error_time_total": { + "name": "Samlet tid i fejl" + }, + "maintenance_status": { + "name": "Vedligeholdelsesstatus", + "state": { + "ok": "OK", + "blade_service_due": "Knivservice påkrævet", + "battery_service_due": "Batteriservice påkrævet" + } + }, + "pitch": { + "name": "Pitch" + }, + "roll": { + "name": "Roll" + }, + "yaw": { + "name": "Yaw" + }, + "last_update": { + "name": "Seneste opdatering" + }, + "last_update_age": { + "name": "Tid siden seneste opdatering" + } + }, + "binary_sensor": { + "online": { + "name": "Online" + }, + "iot_registered": { + "name": "IoT registreret" + }, + "mqtt_registered": { + "name": "MQTT registreret" + }, + "locked": { + "name": "Låst" + }, + "rain_triggered": { + "name": "Regn registreret" + }, + "robot_lifted": { + "name": "Robot løftet" + }, + "off_limits_enabled": { + "name": "Forbudte zoner aktiveret" + }, + "acs_enabled": { + "name": "ACS aktiveret" + }, + "party_mode_enabled": { + "name": "Festtilstand aktiveret" + }, + "pause_mode_enabled": { + "name": "Pausetilstand aktiveret" + }, + "smart_edge_cut": { + "name": "Smart kantklipning" + }, + "save_hedgehogs": { + "name": "Red pindsvinene" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Automatisk firmwareopdatering" + }, + "lock": { + "name": "Lås" + }, + "native_schedule": { + "name": "Indbygget tidsplan" + }, + "smart_edge_cut": { + "name": "Smart kantklipning" + }, + "save_hedgehogs": { + "name": "Red pindsvinene" + }, + "one_time_mowing_edge_cut": { + "name": "Kantklipning" + } + }, + "button": { + "refresh": { + "name": "Opdater" + }, + "reset_blade_counter": { + "name": "Nulstil knivtid" + }, + "reset_battery_cycle_counter": { + "name": "Nulstil battericyklusser" + }, + "start_edge_cut": { + "name": "Start kantklipning" + }, + "start_one_time_mowing": { + "name": "Start engangsslåning" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Regnforsinkelse" + }, + "time_extension": { + "name": "Tidsforlængelse" + }, + "lawn_area": { + "name": "Plænens areal" + }, + "lawn_perimeter": { + "name": "Plænens omkreds" + }, + "one_time_mowing_runtime": { + "name": "Varighed af engangsslåning" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Klippezone" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "RTK-kort" + } + }, + "calendar": { + "schedule": { + "name": "Slåningsplan" + } + }, + "device_tracker": { + "rtk_position": { + "name": "RTK-position" + } + } + } +} diff --git a/custom_components/worx_vision_cloud/translations/de.json b/custom_components/worx_vision_cloud/translations/de.json new file mode 100644 index 0000000..32306e8 --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/de.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Verwenden Sie dieselben Anmeldedaten wie in der Worx-Landroid-App.", + "data": { + "email": "E-Mail-Adresse", + "password": "Passwort", + "cloud": "Cloud", + "verify_ssl": "SSL-Zertifikat überprüfen", + "expose_raw_entities": "Alle Roh-Payload-Felder als Entitäten bereitstellen" + } + } + }, + "error": { + "cannot_connect": "Verbindung zur Worx Cloud fehlgeschlagen.", + "invalid_auth": "Ungültiger Benutzername oder ungültiges Passwort.", + "rate_limited": "Anfragelimit der Worx Cloud erreicht. Bitte später erneut versuchen." + }, + "abort": { + "already_configured": "Dieses Konto ist bereits konfiguriert." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Akku" + }, + "status": { + "name": "Status", + "state": { + "home": "In der Station", + "leaving_home": "Verlässt die Station", + "going_home": "Rückkehr zur Station", + "mowing": "Mäht", + "edge_cutting": "Kantenschnitt", + "charging": "Lädt", + "paused": "Pausiert", + "idle": "Inaktiv", + "manual_stop": "Manuell gestoppt", + "rain_delay": "Regenverzögerung", + "locked": "Gesperrt", + "error": "Fehler", + "no_error": "Kein Fehler", + "offline": "Offline" + } + }, + "error": { + "name": "Fehler", + "state": { + "home": "In der Station", + "leaving_home": "Verlässt die Station", + "going_home": "Rückkehr zur Station", + "mowing": "Mäht", + "edge_cutting": "Kantenschnitt", + "charging": "Lädt", + "paused": "Pausiert", + "idle": "Inaktiv", + "manual_stop": "Manuell gestoppt", + "rain_delay": "Regenverzögerung", + "locked": "Gesperrt", + "error": "Fehler", + "no_error": "Kein Fehler", + "offline": "Offline" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Aktuelle Zone" + }, + "schedule": { + "name": "Zeitplan" + }, + "mowing_readiness": { + "name": "Mähbereitschaft", + "state": { + "ready": "Bereit", + "mowing": "Mäht", + "charging": "Lädt", + "battery_low": "Akku niedrig", + "rain_delay": "Regenverzögerung", + "error": "Fehler", + "locked": "Gesperrt", + "offline": "Offline" + } + }, + "cloud_connection": { + "name": "Cloud-Verbindung", + "state": { + "ok": "Verbunden", + "check": "Wird geprüft", + "offline": "Offline" + } + }, + "api_capabilities": { + "name": "API-Funktionen" + }, + "push_notifications": { + "name": "Push-Benachrichtigungen" + }, + "daily_progress": { + "name": "Tagesfortschritt" + }, + "remaining_progress": { + "name": "Verbleibend zu mähen" + }, + "area_mowed_today": { + "name": "Heute gemähte Fläche" + }, + "next_schedule": { + "name": "Nächster Zeitplan" + }, + "area_mowed_total": { + "name": "Insgesamt gemähte Fläche" + }, + "estimated_area_mowed_today": { + "name": "Geschätzte heute gemähte Fläche" + }, + "estimated_daily_progress": { + "name": "Geschätzter Tagesfortschritt" + }, + "lawn_area": { + "name": "Rasenfläche" + }, + "mowing_efficiency": { + "name": "Mäheffizienz" + }, + "rtk_map": { + "name": "RTK-Karte" + }, + "rtk_trail_points": { + "name": "RTK-Spurpunkte" + }, + "rtk_address": { + "name": "RTK-Adresse" + }, + "rain_delay": { + "name": "Regenverzögerung" + }, + "rain_remaining": { + "name": "Verbleibende Regenverzögerung" + }, + "battery_voltage": { + "name": "Akkuspannung" + }, + "battery_temperature": { + "name": "Akkutemperatur" + }, + "battery_cycles_total": { + "name": "Akkuzyklen (gesamt)" + }, + "battery_cycles_since_reset": { + "name": "Akkuzyklen seit Zurücksetzen" + }, + "battery_cycles_reset_at": { + "name": "Letztes Zurücksetzen der Akkuzyklen" + }, + "blade_runtime_total": { + "name": "Messer-Laufzeit (gesamt)" + }, + "blade_runtime_current": { + "name": "Messer-Laufzeit (aktuell)" + }, + "blade_runtime_reset_at": { + "name": "Letztes Zurücksetzen der Messer-Laufzeit" + }, + "mower_runtime_total": { + "name": "Gesamtlaufzeit" + }, + "mower_home_time_total": { + "name": "Gesamtzeit in der Station" + }, + "mower_charging_time_total": { + "name": "Gesamtladezeit" + }, + "mower_error_time_total": { + "name": "Gesamtzeit im Fehlerzustand" + }, + "maintenance_status": { + "name": "Wartungsstatus", + "state": { + "ok": "OK", + "blade_service_due": "Messerwartung fällig", + "battery_service_due": "Akkuwartung fällig" + } + }, + "pitch": { + "name": "Nicken" + }, + "roll": { + "name": "Rollen" + }, + "yaw": { + "name": "Gieren" + }, + "last_update": { + "name": "Letzte Aktualisierung" + }, + "last_update_age": { + "name": "Alter der letzten Aktualisierung" + } + }, + "binary_sensor": { + "online": { + "name": "Online" + }, + "iot_registered": { + "name": "IoT registriert" + }, + "mqtt_registered": { + "name": "MQTT registriert" + }, + "locked": { + "name": "Gesperrt" + }, + "rain_triggered": { + "name": "Regen erkannt" + }, + "robot_lifted": { + "name": "Roboter angehoben" + }, + "off_limits_enabled": { + "name": "Sperrzonen aktiviert" + }, + "acs_enabled": { + "name": "ACS aktiviert" + }, + "party_mode_enabled": { + "name": "Partymodus aktiviert" + }, + "pause_mode_enabled": { + "name": "Pausenmodus aktiviert" + }, + "smart_edge_cut": { + "name": "Intelligenter Kantenschnitt" + }, + "save_hedgehogs": { + "name": "Igel retten" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Firmware automatisch aktualisieren" + }, + "lock": { + "name": "Sperren" + }, + "native_schedule": { + "name": "Nativer Zeitplan" + }, + "smart_edge_cut": { + "name": "Intelligenter Kantenschnitt" + }, + "save_hedgehogs": { + "name": "Igel retten" + }, + "one_time_mowing_edge_cut": { + "name": "Kantenschnitt" + } + }, + "button": { + "refresh": { + "name": "Aktualisieren" + }, + "reset_blade_counter": { + "name": "Messer-Laufzeit zurücksetzen" + }, + "reset_battery_cycle_counter": { + "name": "Akkuzyklen zurücksetzen" + }, + "start_edge_cut": { + "name": "Kantenschnitt starten" + }, + "start_one_time_mowing": { + "name": "Einmaliges Mähen starten" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Regenverzögerung" + }, + "time_extension": { + "name": "Zeitverlängerung" + }, + "lawn_area": { + "name": "Rasenfläche" + }, + "lawn_perimeter": { + "name": "Rasenumfang" + }, + "one_time_mowing_runtime": { + "name": "Laufzeit für einmaliges Mähen" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Mähzone" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "RTK-Karte" + } + }, + "calendar": { + "schedule": { + "name": "Mähplan" + } + }, + "device_tracker": { + "rtk_position": { + "name": "RTK-Position" + } + } + } +} diff --git a/custom_components/worx_vision_cloud/translations/en.json b/custom_components/worx_vision_cloud/translations/en.json index d878c30..597143f 100644 --- a/custom_components/worx_vision_cloud/translations/en.json +++ b/custom_components/worx_vision_cloud/translations/en.json @@ -24,20 +24,47 @@ } }, "entity": { - "lawn_mower": { - "mower": { - "name": "Mower" - } - }, "sensor": { "battery_percent": { "name": "Battery" }, "status": { - "name": "Status" + "name": "Status", + "state": { + "home": "Docked", + "leaving_home": "Leaving base", + "going_home": "Returning to base", + "mowing": "Mowing", + "edge_cutting": "Edge cutting", + "charging": "Charging", + "paused": "Paused", + "idle": "Idle", + "manual_stop": "Manual stop", + "rain_delay": "Rain delay", + "locked": "Locked", + "error": "Error", + "no_error": "No error", + "offline": "Offline" + } }, "error": { - "name": "Error" + "name": "Error", + "state": { + "home": "Docked", + "leaving_home": "Leaving base", + "going_home": "Returning to base", + "mowing": "Mowing", + "edge_cutting": "Edge cutting", + "charging": "Charging", + "paused": "Paused", + "idle": "Idle", + "manual_stop": "Manual stop", + "rain_delay": "Rain delay", + "locked": "Locked", + "error": "Error", + "no_error": "No error", + "offline": "Offline" + } }, "rssi": { "name": "RSSI" @@ -49,10 +76,25 @@ "name": "Schedule" }, "mowing_readiness": { - "name": "Mowing readiness" + "name": "Mowing readiness", + "state": { + "ready": "Ready", + "mowing": "Mowing", + "charging": "Charging", + "battery_low": "Battery low", + "rain_delay": "Rain delay", + "error": "Error", + "locked": "Locked", + "offline": "Offline" + } }, "cloud_connection": { - "name": "Cloud connection" + "name": "Cloud connection", + "state": { + "ok": "Connected", + "check": "Checking", + "offline": "Offline" + } }, "api_capabilities": { "name": "API capabilities" @@ -69,6 +111,18 @@ "area_mowed_today": { "name": "Area mowed today" }, + "next_schedule": { + "name": "Next schedule" + }, + "area_mowed_total": { + "name": "Total area mowed" + }, + "estimated_area_mowed_today": { + "name": "Estimated area mowed today" + }, + "estimated_daily_progress": { + "name": "Estimated daily progress" + }, "lawn_area": { "name": "Lawn area" }, @@ -127,7 +181,12 @@ "name": "Mower error time total" }, "maintenance_status": { - "name": "Maintenance status" + "name": "Maintenance status", + "state": { + "ok": "OK", + "blade_service_due": "Blade service due", + "battery_service_due": "Battery service due" + } }, "pitch": { "name": "Pitch" @@ -239,7 +298,7 @@ }, "select": { "one_time_mowing_zones": { - "name": "One-time mowing zones" + "name": "Mowing zone" } }, "update": { @@ -256,6 +315,11 @@ "schedule": { "name": "Mowing schedule" } + }, + "device_tracker": { + "rtk_position": { + "name": "RTK position" + } } } } diff --git a/custom_components/worx_vision_cloud/translations/es.json b/custom_components/worx_vision_cloud/translations/es.json new file mode 100644 index 0000000..1bb3910 --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/es.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Usa las mismas credenciales que en la app Worx Landroid.", + "data": { + "email": "Correo electrónico", + "password": "Contraseña", + "cloud": "Nube", + "verify_ssl": "Verificar SSL", + "expose_raw_entities": "Exponer todos los campos de datos brutos como entidades" + } + } + }, + "error": { + "cannot_connect": "No se pudo conectar con Worx Cloud.", + "invalid_auth": "Usuario o contraseña incorrectos.", + "rate_limited": "Límite de solicitudes de Worx Cloud alcanzado. Inténtalo más tarde." + }, + "abort": { + "already_configured": "Esta cuenta ya está configurada." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Batería" + }, + "status": { + "name": "Estado", + "state": { + "home": "En la base", + "leaving_home": "Saliendo de la base", + "going_home": "Regresando a la base", + "mowing": "Cortando", + "edge_cutting": "Corte de bordes", + "charging": "Cargando", + "paused": "En pausa", + "idle": "Inactivo", + "manual_stop": "Detenido manualmente", + "rain_delay": "Retraso por lluvia", + "locked": "Bloqueado", + "error": "Error", + "no_error": "Sin errores", + "offline": "Sin conexión" + } + }, + "error": { + "name": "Error", + "state": { + "home": "En la base", + "leaving_home": "Saliendo de la base", + "going_home": "Regresando a la base", + "mowing": "Cortando", + "edge_cutting": "Corte de bordes", + "charging": "Cargando", + "paused": "En pausa", + "idle": "Inactivo", + "manual_stop": "Detenido manualmente", + "rain_delay": "Retraso por lluvia", + "locked": "Bloqueado", + "error": "Error", + "no_error": "Sin errores", + "offline": "Sin conexión" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Zona actual" + }, + "schedule": { + "name": "Programación" + }, + "mowing_readiness": { + "name": "Disponibilidad para cortar", + "state": { + "ready": "Lista", + "mowing": "Cortando", + "charging": "Cargando", + "battery_low": "Batería baja", + "rain_delay": "Retraso por lluvia", + "error": "Error", + "locked": "Bloqueado", + "offline": "Sin conexión" + } + }, + "cloud_connection": { + "name": "Conexión a la nube", + "state": { + "ok": "Conectado", + "check": "Comprobando", + "offline": "Sin conexión" + } + }, + "api_capabilities": { + "name": "Capacidades de la API" + }, + "push_notifications": { + "name": "Notificaciones push" + }, + "daily_progress": { + "name": "Progreso diario" + }, + "remaining_progress": { + "name": "Restante por cortar" + }, + "area_mowed_today": { + "name": "Superficie cortada hoy" + }, + "next_schedule": { + "name": "Próximo corte" + }, + "area_mowed_total": { + "name": "Superficie total cortada" + }, + "estimated_area_mowed_today": { + "name": "Superficie estimada cortada hoy" + }, + "estimated_daily_progress": { + "name": "Progreso diario estimado" + }, + "lawn_area": { + "name": "Superficie del césped" + }, + "mowing_efficiency": { + "name": "Eficiencia de corte" + }, + "rtk_map": { + "name": "Mapa RTK" + }, + "rtk_trail_points": { + "name": "Puntos de trayectoria RTK" + }, + "rtk_address": { + "name": "Dirección RTK" + }, + "rain_delay": { + "name": "Retraso por lluvia" + }, + "rain_remaining": { + "name": "Retraso por lluvia restante" + }, + "battery_voltage": { + "name": "Voltaje de la batería" + }, + "battery_temperature": { + "name": "Temperatura de la batería" + }, + "battery_cycles_total": { + "name": "Ciclos de batería (total)" + }, + "battery_cycles_since_reset": { + "name": "Ciclos de batería desde el reinicio" + }, + "battery_cycles_reset_at": { + "name": "Último reinicio de ciclos de batería" + }, + "blade_runtime_total": { + "name": "Tiempo de funcionamiento de cuchillas (total)" + }, + "blade_runtime_current": { + "name": "Tiempo de funcionamiento de cuchillas (actual)" + }, + "blade_runtime_reset_at": { + "name": "Último reinicio del tiempo de cuchillas" + }, + "mower_runtime_total": { + "name": "Tiempo total de funcionamiento" + }, + "mower_home_time_total": { + "name": "Tiempo total en la base" + }, + "mower_charging_time_total": { + "name": "Tiempo total de carga" + }, + "mower_error_time_total": { + "name": "Tiempo total en error" + }, + "maintenance_status": { + "name": "Estado de mantenimiento", + "state": { + "ok": "OK", + "blade_service_due": "Revisión de cuchillas pendiente", + "battery_service_due": "Revisión de batería pendiente" + } + }, + "pitch": { + "name": "Cabeceo" + }, + "roll": { + "name": "Alabeo" + }, + "yaw": { + "name": "Guiñada" + }, + "last_update": { + "name": "Última actualización" + }, + "last_update_age": { + "name": "Antigüedad de la última actualización" + } + }, + "binary_sensor": { + "online": { + "name": "En línea" + }, + "iot_registered": { + "name": "IoT registrado" + }, + "mqtt_registered": { + "name": "MQTT registrado" + }, + "locked": { + "name": "Bloqueado" + }, + "rain_triggered": { + "name": "Lluvia detectada" + }, + "robot_lifted": { + "name": "Robot levantado" + }, + "off_limits_enabled": { + "name": "Zonas prohibidas activadas" + }, + "acs_enabled": { + "name": "ACS activado" + }, + "party_mode_enabled": { + "name": "Modo fiesta activado" + }, + "pause_mode_enabled": { + "name": "Modo pausa activado" + }, + "smart_edge_cut": { + "name": "Corte de bordes inteligente" + }, + "save_hedgehogs": { + "name": "Modo erizos" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Actualización automática de firmware" + }, + "lock": { + "name": "Bloqueo" + }, + "native_schedule": { + "name": "Programación nativa" + }, + "smart_edge_cut": { + "name": "Corte de bordes inteligente" + }, + "save_hedgehogs": { + "name": "Modo erizos" + }, + "one_time_mowing_edge_cut": { + "name": "Corte de bordes" + } + }, + "button": { + "refresh": { + "name": "Actualizar" + }, + "reset_blade_counter": { + "name": "Reiniciar tiempo de cuchillas" + }, + "reset_battery_cycle_counter": { + "name": "Reiniciar ciclos de batería" + }, + "start_edge_cut": { + "name": "Iniciar corte de bordes" + }, + "start_one_time_mowing": { + "name": "Iniciar corte puntual" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Retraso por lluvia" + }, + "time_extension": { + "name": "Prolongación de tiempo" + }, + "lawn_area": { + "name": "Superficie del césped" + }, + "lawn_perimeter": { + "name": "Perímetro del césped" + }, + "one_time_mowing_runtime": { + "name": "Duración del corte puntual" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Zona de corte" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "Mapa RTK" + } + }, + "calendar": { + "schedule": { + "name": "Programación de corte" + } + }, + "device_tracker": { + "rtk_position": { + "name": "Posición RTK" + } + } + } +} diff --git a/custom_components/worx_vision_cloud/translations/fr.json b/custom_components/worx_vision_cloud/translations/fr.json new file mode 100644 index 0000000..98ca3e7 --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/fr.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Utilisez les mêmes identifiants que dans l'application Worx Landroid.", + "data": { + "email": "Adresse e-mail", + "password": "Mot de passe", + "cloud": "Cloud", + "verify_ssl": "Vérifier le certificat SSL", + "expose_raw_entities": "Exposer tous les champs bruts du payload en tant qu'entités" + } + } + }, + "error": { + "cannot_connect": "Impossible de se connecter au Cloud Worx.", + "invalid_auth": "Identifiant ou mot de passe incorrect.", + "rate_limited": "Limite de requêtes du Cloud Worx atteinte. Réessayez plus tard." + }, + "abort": { + "already_configured": "Ce compte est déjà configuré." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Batterie" + }, + "status": { + "name": "État", + "state": { + "home": "En station", + "leaving_home": "Sortie de la base", + "going_home": "Retour à la base", + "mowing": "En train de tondre", + "edge_cutting": "Coupe de bordure", + "charging": "En charge", + "paused": "En pause", + "idle": "Inactive", + "manual_stop": "Arrêt manuel", + "rain_delay": "Délai pluie", + "locked": "Verrouillée", + "error": "Erreur", + "no_error": "Aucune erreur", + "offline": "Hors ligne" + } + }, + "error": { + "name": "Erreur", + "state": { + "home": "En station", + "leaving_home": "Sortie de la base", + "going_home": "Retour à la base", + "mowing": "En train de tondre", + "edge_cutting": "Coupe de bordure", + "charging": "En charge", + "paused": "En pause", + "idle": "Inactive", + "manual_stop": "Arrêt manuel", + "rain_delay": "Délai pluie", + "locked": "Verrouillée", + "error": "Erreur", + "no_error": "Aucune erreur", + "offline": "Hors ligne" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Zone actuelle" + }, + "schedule": { + "name": "Programme" + }, + "mowing_readiness": { + "name": "Aptitude à la tonte", + "state": { + "ready": "Prête", + "mowing": "En train de tondre", + "charging": "En charge", + "battery_low": "Batterie faible", + "rain_delay": "Délai pluie", + "error": "Erreur", + "locked": "Verrouillée", + "offline": "Hors ligne" + } + }, + "cloud_connection": { + "name": "Connexion au cloud", + "state": { + "ok": "Connecté", + "check": "Vérification", + "offline": "Hors ligne" + } + }, + "api_capabilities": { + "name": "Capacités de l'API" + }, + "push_notifications": { + "name": "Notifications push" + }, + "daily_progress": { + "name": "Progression quotidienne" + }, + "remaining_progress": { + "name": "Reste à tondre" + }, + "area_mowed_today": { + "name": "Surface tondue aujourd'hui" + }, + "next_schedule": { + "name": "Prochaine tonte" + }, + "area_mowed_total": { + "name": "Surface totale tondue" + }, + "estimated_area_mowed_today": { + "name": "Surface estimée tondue aujourd'hui" + }, + "estimated_daily_progress": { + "name": "Progression quotidienne estimée" + }, + "lawn_area": { + "name": "Surface de la pelouse" + }, + "mowing_efficiency": { + "name": "Efficacité de tonte" + }, + "rtk_map": { + "name": "Carte RTK" + }, + "rtk_trail_points": { + "name": "Points de trace RTK" + }, + "rtk_address": { + "name": "Adresse RTK" + }, + "rain_delay": { + "name": "Délai pluie" + }, + "rain_remaining": { + "name": "Délai pluie restant" + }, + "battery_voltage": { + "name": "Tension de la batterie" + }, + "battery_temperature": { + "name": "Température de la batterie" + }, + "battery_cycles_total": { + "name": "Cycles de batterie (total)" + }, + "battery_cycles_since_reset": { + "name": "Cycles de batterie depuis réinitialisation" + }, + "battery_cycles_reset_at": { + "name": "Dernière réinitialisation des cycles de batterie" + }, + "blade_runtime_total": { + "name": "Temps de fonctionnement des lames (total)" + }, + "blade_runtime_current": { + "name": "Temps de fonctionnement des lames (actuel)" + }, + "blade_runtime_reset_at": { + "name": "Dernière réinitialisation du temps des lames" + }, + "mower_runtime_total": { + "name": "Temps de fonctionnement total" + }, + "mower_home_time_total": { + "name": "Temps total en station" + }, + "mower_charging_time_total": { + "name": "Temps de charge total" + }, + "mower_error_time_total": { + "name": "Temps total en erreur" + }, + "maintenance_status": { + "name": "État de maintenance", + "state": { + "ok": "OK", + "blade_service_due": "Entretien des lames requis", + "battery_service_due": "Entretien de la batterie requis" + } + }, + "pitch": { + "name": "Tangage" + }, + "roll": { + "name": "Roulis" + }, + "yaw": { + "name": "Lacet" + }, + "last_update": { + "name": "Dernière mise à jour" + }, + "last_update_age": { + "name": "Ancienneté de la dernière mise à jour" + } + }, + "binary_sensor": { + "online": { + "name": "En ligne" + }, + "iot_registered": { + "name": "IoT enregistré" + }, + "mqtt_registered": { + "name": "MQTT enregistré" + }, + "locked": { + "name": "Verrouillé" + }, + "rain_triggered": { + "name": "Pluie détectée" + }, + "robot_lifted": { + "name": "Robot soulevé" + }, + "off_limits_enabled": { + "name": "Zones interdites activées" + }, + "acs_enabled": { + "name": "ACS activé" + }, + "party_mode_enabled": { + "name": "Mode festif activé" + }, + "pause_mode_enabled": { + "name": "Mode pause activé" + }, + "smart_edge_cut": { + "name": "Coupe de bordure intelligente" + }, + "save_hedgehogs": { + "name": "Sauvons les hérissons" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Mise à jour automatique du firmware" + }, + "lock": { + "name": "Verrouillage" + }, + "native_schedule": { + "name": "Programme natif" + }, + "smart_edge_cut": { + "name": "Coupe de bordure intelligente" + }, + "save_hedgehogs": { + "name": "Sauvons les hérissons" + }, + "one_time_mowing_edge_cut": { + "name": "Coupe de bordure" + } + }, + "button": { + "refresh": { + "name": "Actualiser" + }, + "reset_blade_counter": { + "name": "Réinitialiser le temps des lames" + }, + "reset_battery_cycle_counter": { + "name": "Réinitialiser les cycles de batterie" + }, + "start_edge_cut": { + "name": "Démarrer la coupe de bordure" + }, + "start_one_time_mowing": { + "name": "Démarrer la tonte unique" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Délai pluie" + }, + "time_extension": { + "name": "Prolongation de durée" + }, + "lawn_area": { + "name": "Surface de la pelouse" + }, + "lawn_perimeter": { + "name": "Périmètre de la pelouse" + }, + "one_time_mowing_runtime": { + "name": "Durée de la tonte unique" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Zone de tonte" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "Carte RTK" + } + }, + "calendar": { + "schedule": { + "name": "Programme de tonte" + } + }, + "device_tracker": { + "rtk_position": { + "name": "Position RTK" + } + } + } +} diff --git a/custom_components/worx_vision_cloud/translations/it.json b/custom_components/worx_vision_cloud/translations/it.json new file mode 100644 index 0000000..25b544d --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/it.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Usa le stesse credenziali dell'app Worx Landroid.", + "data": { + "email": "E-mail", + "password": "Password", + "cloud": "Cloud", + "verify_ssl": "Verifica SSL", + "expose_raw_entities": "Esponi tutti i campi payload grezzi come entità" + } + } + }, + "error": { + "cannot_connect": "Impossibile connettersi a Worx Cloud.", + "invalid_auth": "Nome utente o password non validi.", + "rate_limited": "Limite di richieste Worx Cloud raggiunto. Riprova più tardi." + }, + "abort": { + "already_configured": "Questo account è già configurato." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Batteria" + }, + "status": { + "name": "Stato", + "state": { + "home": "In base", + "leaving_home": "Uscita dalla base", + "going_home": "Rientro alla base", + "mowing": "In taglio", + "edge_cutting": "Taglio bordi", + "charging": "In carica", + "paused": "In pausa", + "idle": "Inattivo", + "manual_stop": "Arresto manuale", + "rain_delay": "Ritardo pioggia", + "locked": "Bloccato", + "error": "Errore", + "no_error": "Nessun errore", + "offline": "Offline" + } + }, + "error": { + "name": "Errore", + "state": { + "home": "In base", + "leaving_home": "Uscita dalla base", + "going_home": "Rientro alla base", + "mowing": "In taglio", + "edge_cutting": "Taglio bordi", + "charging": "In carica", + "paused": "In pausa", + "idle": "Inattivo", + "manual_stop": "Arresto manuale", + "rain_delay": "Ritardo pioggia", + "locked": "Bloccato", + "error": "Errore", + "no_error": "Nessun errore", + "offline": "Offline" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Zona attuale" + }, + "schedule": { + "name": "Programma" + }, + "mowing_readiness": { + "name": "Prontezza al taglio", + "state": { + "ready": "Pronto", + "mowing": "In taglio", + "charging": "In carica", + "battery_low": "Batteria scarica", + "rain_delay": "Ritardo pioggia", + "error": "Errore", + "locked": "Bloccato", + "offline": "Offline" + } + }, + "cloud_connection": { + "name": "Connessione al cloud", + "state": { + "ok": "Connesso", + "check": "Verifica in corso", + "offline": "Offline" + } + }, + "api_capabilities": { + "name": "Funzionalità API" + }, + "push_notifications": { + "name": "Notifiche push" + }, + "daily_progress": { + "name": "Avanzamento giornaliero" + }, + "remaining_progress": { + "name": "Rimanente da tagliare" + }, + "area_mowed_today": { + "name": "Superficie tagliata oggi" + }, + "next_schedule": { + "name": "Prossimo taglio" + }, + "area_mowed_total": { + "name": "Superficie totale tagliata" + }, + "estimated_area_mowed_today": { + "name": "Superficie stimata tagliata oggi" + }, + "estimated_daily_progress": { + "name": "Avanzamento giornaliero stimato" + }, + "lawn_area": { + "name": "Superficie del prato" + }, + "mowing_efficiency": { + "name": "Efficienza di taglio" + }, + "rtk_map": { + "name": "Mappa RTK" + }, + "rtk_trail_points": { + "name": "Punti traccia RTK" + }, + "rtk_address": { + "name": "Indirizzo RTK" + }, + "rain_delay": { + "name": "Ritardo pioggia" + }, + "rain_remaining": { + "name": "Ritardo pioggia residuo" + }, + "battery_voltage": { + "name": "Tensione della batteria" + }, + "battery_temperature": { + "name": "Temperatura della batteria" + }, + "battery_cycles_total": { + "name": "Cicli batteria (totale)" + }, + "battery_cycles_since_reset": { + "name": "Cicli batteria dall'ultimo reset" + }, + "battery_cycles_reset_at": { + "name": "Ultimo reset cicli batteria" + }, + "blade_runtime_total": { + "name": "Tempo di funzionamento lame (totale)" + }, + "blade_runtime_current": { + "name": "Tempo di funzionamento lame (attuale)" + }, + "blade_runtime_reset_at": { + "name": "Ultimo reset tempo lame" + }, + "mower_runtime_total": { + "name": "Tempo di funzionamento totale" + }, + "mower_home_time_total": { + "name": "Tempo totale in base" + }, + "mower_charging_time_total": { + "name": "Tempo totale in carica" + }, + "mower_error_time_total": { + "name": "Tempo totale in errore" + }, + "maintenance_status": { + "name": "Stato manutenzione", + "state": { + "ok": "OK", + "blade_service_due": "Manutenzione lame necessaria", + "battery_service_due": "Manutenzione batteria necessaria" + } + }, + "pitch": { + "name": "Beccheggio" + }, + "roll": { + "name": "Rollio" + }, + "yaw": { + "name": "Imbardata" + }, + "last_update": { + "name": "Ultimo aggiornamento" + }, + "last_update_age": { + "name": "Tempo dall'ultimo aggiornamento" + } + }, + "binary_sensor": { + "online": { + "name": "Online" + }, + "iot_registered": { + "name": "IoT registrato" + }, + "mqtt_registered": { + "name": "MQTT registrato" + }, + "locked": { + "name": "Bloccato" + }, + "rain_triggered": { + "name": "Pioggia rilevata" + }, + "robot_lifted": { + "name": "Robot sollevato" + }, + "off_limits_enabled": { + "name": "Zone vietate attive" + }, + "acs_enabled": { + "name": "ACS attivo" + }, + "party_mode_enabled": { + "name": "Modalità festa attiva" + }, + "pause_mode_enabled": { + "name": "Modalità pausa attiva" + }, + "smart_edge_cut": { + "name": "Taglio bordi intelligente" + }, + "save_hedgehogs": { + "name": "Salva i ricci" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Aggiornamento automatico firmware" + }, + "lock": { + "name": "Blocco" + }, + "native_schedule": { + "name": "Programma nativo" + }, + "smart_edge_cut": { + "name": "Taglio bordi intelligente" + }, + "save_hedgehogs": { + "name": "Salva i ricci" + }, + "one_time_mowing_edge_cut": { + "name": "Taglio bordi" + } + }, + "button": { + "refresh": { + "name": "Aggiorna" + }, + "reset_blade_counter": { + "name": "Reimposta tempo lame" + }, + "reset_battery_cycle_counter": { + "name": "Reimposta cicli batteria" + }, + "start_edge_cut": { + "name": "Avvia taglio bordi" + }, + "start_one_time_mowing": { + "name": "Avvia taglio singolo" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Ritardo pioggia" + }, + "time_extension": { + "name": "Estensione tempo" + }, + "lawn_area": { + "name": "Superficie del prato" + }, + "lawn_perimeter": { + "name": "Perimetro del prato" + }, + "one_time_mowing_runtime": { + "name": "Durata taglio singolo" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Zona di taglio" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "Mappa RTK" + } + }, + "calendar": { + "schedule": { + "name": "Programma di taglio" + } + }, + "device_tracker": { + "rtk_position": { + "name": "Posizione RTK" + } + } + } +} diff --git a/custom_components/worx_vision_cloud/translations/nl.json b/custom_components/worx_vision_cloud/translations/nl.json new file mode 100644 index 0000000..f4de316 --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/nl.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Gebruik dezelfde inloggegevens als in de Worx Landroid-app.", + "data": { + "email": "E-mailadres", + "password": "Wachtwoord", + "cloud": "Cloud", + "verify_ssl": "SSL verifiëren", + "expose_raw_entities": "Alle ruwe payloadvelden als entiteiten weergeven" + } + } + }, + "error": { + "cannot_connect": "Kon geen verbinding maken met Worx Cloud.", + "invalid_auth": "Ongeldige gebruikersnaam of wachtwoord.", + "rate_limited": "Snelheidslimiet van Worx Cloud bereikt. Probeer het later opnieuw." + }, + "abort": { + "already_configured": "Dit account is al geconfigureerd." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Batterij" + }, + "status": { + "name": "Status", + "state": { + "home": "In het laadstation", + "leaving_home": "Verlaat basisstation", + "going_home": "Terugkeer naar basisstation", + "mowing": "Maait", + "edge_cutting": "Randmaaien", + "charging": "Opladen", + "paused": "Gepauzeerd", + "idle": "Inactief", + "manual_stop": "Handmatig gestopt", + "rain_delay": "Regenvertraging", + "locked": "Vergrendeld", + "error": "Fout", + "no_error": "Geen fout", + "offline": "Offline" + } + }, + "error": { + "name": "Fout", + "state": { + "home": "In het laadstation", + "leaving_home": "Verlaat basisstation", + "going_home": "Terugkeer naar basisstation", + "mowing": "Maait", + "edge_cutting": "Randmaaien", + "charging": "Opladen", + "paused": "Gepauzeerd", + "idle": "Inactief", + "manual_stop": "Handmatig gestopt", + "rain_delay": "Regenvertraging", + "locked": "Vergrendeld", + "error": "Fout", + "no_error": "Geen fout", + "offline": "Offline" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Huidige zone" + }, + "schedule": { + "name": "Schema" + }, + "mowing_readiness": { + "name": "Maaigereedheid", + "state": { + "ready": "Gereed", + "mowing": "Maait", + "charging": "Opladen", + "battery_low": "Batterij bijna leeg", + "rain_delay": "Regenvertraging", + "error": "Fout", + "locked": "Vergrendeld", + "offline": "Offline" + } + }, + "cloud_connection": { + "name": "Cloudverbinding", + "state": { + "ok": "Verbonden", + "check": "Controleren", + "offline": "Offline" + } + }, + "api_capabilities": { + "name": "API-mogelijkheden" + }, + "push_notifications": { + "name": "Pushmeldingen" + }, + "daily_progress": { + "name": "Dagvoortgang" + }, + "remaining_progress": { + "name": "Nog te maaien" + }, + "area_mowed_today": { + "name": "Vandaag gemaaid oppervlak" + }, + "next_schedule": { + "name": "Volgende maaibeurt" + }, + "area_mowed_total": { + "name": "Totaal gemaaid oppervlak" + }, + "estimated_area_mowed_today": { + "name": "Geschatte vandaag gemaaid oppervlak" + }, + "estimated_daily_progress": { + "name": "Geschatte dagvoortgang" + }, + "lawn_area": { + "name": "Gazonoppervlak" + }, + "mowing_efficiency": { + "name": "Maai-efficiëntie" + }, + "rtk_map": { + "name": "RTK-kaart" + }, + "rtk_trail_points": { + "name": "RTK-spoorpunten" + }, + "rtk_address": { + "name": "RTK-adres" + }, + "rain_delay": { + "name": "Regenvertraging" + }, + "rain_remaining": { + "name": "Resterende regenvertraging" + }, + "battery_voltage": { + "name": "Batterijspanning" + }, + "battery_temperature": { + "name": "Batterijtemperatuur" + }, + "battery_cycles_total": { + "name": "Batterijcycli (totaal)" + }, + "battery_cycles_since_reset": { + "name": "Batterijcycli sinds reset" + }, + "battery_cycles_reset_at": { + "name": "Laatste reset batterijcycli" + }, + "blade_runtime_total": { + "name": "Meslooptijd (totaal)" + }, + "blade_runtime_current": { + "name": "Meslooptijd (huidig)" + }, + "blade_runtime_reset_at": { + "name": "Laatste reset meslooptijd" + }, + "mower_runtime_total": { + "name": "Totale looptijd" + }, + "mower_home_time_total": { + "name": "Totale tijd in basisstation" + }, + "mower_charging_time_total": { + "name": "Totale oplaadtijd" + }, + "mower_error_time_total": { + "name": "Totale tijd in fout" + }, + "maintenance_status": { + "name": "Onderhoudsstatus", + "state": { + "ok": "OK", + "blade_service_due": "Mesonderhoud nodig", + "battery_service_due": "Batterijonderhoud nodig" + } + }, + "pitch": { + "name": "Pitch" + }, + "roll": { + "name": "Roll" + }, + "yaw": { + "name": "Yaw" + }, + "last_update": { + "name": "Laatste update" + }, + "last_update_age": { + "name": "Tijd sinds laatste update" + } + }, + "binary_sensor": { + "online": { + "name": "Online" + }, + "iot_registered": { + "name": "IoT geregistreerd" + }, + "mqtt_registered": { + "name": "MQTT geregistreerd" + }, + "locked": { + "name": "Vergrendeld" + }, + "rain_triggered": { + "name": "Regen gedetecteerd" + }, + "robot_lifted": { + "name": "Robot opgetild" + }, + "off_limits_enabled": { + "name": "Verboden zones ingeschakeld" + }, + "acs_enabled": { + "name": "ACS ingeschakeld" + }, + "party_mode_enabled": { + "name": "Feestmodus ingeschakeld" + }, + "pause_mode_enabled": { + "name": "Pauzemodus ingeschakeld" + }, + "smart_edge_cut": { + "name": "Slim randmaaien" + }, + "save_hedgehogs": { + "name": "Egels redden" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Automatische firmware-update" + }, + "lock": { + "name": "Vergrendelen" + }, + "native_schedule": { + "name": "Native planning" + }, + "smart_edge_cut": { + "name": "Slim randmaaien" + }, + "save_hedgehogs": { + "name": "Egels redden" + }, + "one_time_mowing_edge_cut": { + "name": "Randmaaien" + } + }, + "button": { + "refresh": { + "name": "Vernieuwen" + }, + "reset_blade_counter": { + "name": "Meslooptijd resetten" + }, + "reset_battery_cycle_counter": { + "name": "Batterijcycli resetten" + }, + "start_edge_cut": { + "name": "Randmaaien starten" + }, + "start_one_time_mowing": { + "name": "Eenmalig maaien starten" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Regenvertraging" + }, + "time_extension": { + "name": "Tijdsverlenging" + }, + "lawn_area": { + "name": "Gazonoppervlak" + }, + "lawn_perimeter": { + "name": "Gazonomtrek" + }, + "one_time_mowing_runtime": { + "name": "Duur eenmalig maaien" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Maaizone" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "RTK-kaart" + } + }, + "calendar": { + "schedule": { + "name": "Maaischema" + } + }, + "device_tracker": { + "rtk_position": { + "name": "RTK-positie" + } + } + } +} diff --git a/custom_components/worx_vision_cloud/translations/no.json b/custom_components/worx_vision_cloud/translations/no.json new file mode 100644 index 0000000..b9bb0c0 --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/no.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Bruk samme innlogging som i Worx Landroid-appen.", + "data": { + "email": "E-post", + "password": "Passord", + "cloud": "Sky", + "verify_ssl": "Bekreft SSL", + "expose_raw_entities": "Eksponer alle rå datafelt som enheter" + } + } + }, + "error": { + "cannot_connect": "Kunne ikke koble til Worx Cloud.", + "invalid_auth": "Ugyldig brukernavn eller passord.", + "rate_limited": "Worx Cloud-forespørselsgrense nådd. Prøv igjen senere." + }, + "abort": { + "already_configured": "Denne kontoen er allerede konfigurert." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Batteri" + }, + "status": { + "name": "Status", + "state": { + "home": "Ved basestasjonen", + "leaving_home": "Forlater basestasjonen", + "going_home": "På vei til basestasjonen", + "mowing": "Klipper", + "edge_cutting": "Kantklipping", + "charging": "Lader", + "paused": "Pause", + "idle": "Inaktiv", + "manual_stop": "Manuelt stoppet", + "rain_delay": "Regnforsinkelse", + "locked": "Låst", + "error": "Feil", + "no_error": "Ingen feil", + "offline": "Offline" + } + }, + "error": { + "name": "Feil", + "state": { + "home": "Ved basestasjonen", + "leaving_home": "Forlater basestasjonen", + "going_home": "På vei til basestasjonen", + "mowing": "Klipper", + "edge_cutting": "Kantklipping", + "charging": "Lader", + "paused": "Pause", + "idle": "Inaktiv", + "manual_stop": "Manuelt stoppet", + "rain_delay": "Regnforsinkelse", + "locked": "Låst", + "error": "Feil", + "no_error": "Ingen feil", + "offline": "Offline" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Nåværende sone" + }, + "schedule": { + "name": "Tidsplan" + }, + "mowing_readiness": { + "name": "Klippeklarhet", + "state": { + "ready": "Klar", + "mowing": "Klipper", + "charging": "Lader", + "battery_low": "Lavt batterinivå", + "rain_delay": "Regnforsinkelse", + "error": "Feil", + "locked": "Låst", + "offline": "Offline" + } + }, + "cloud_connection": { + "name": "Skytilkobling", + "state": { + "ok": "Tilkoblet", + "check": "Sjekker", + "offline": "Offline" + } + }, + "api_capabilities": { + "name": "API-funksjoner" + }, + "push_notifications": { + "name": "Push-varsler" + }, + "daily_progress": { + "name": "Dagens fremgang" + }, + "remaining_progress": { + "name": "Gjenstår å klippe" + }, + "area_mowed_today": { + "name": "Klippet areal i dag" + }, + "next_schedule": { + "name": "Neste klipping" + }, + "area_mowed_total": { + "name": "Totalt klippet areal" + }, + "estimated_area_mowed_today": { + "name": "Estimert klippet areal i dag" + }, + "estimated_daily_progress": { + "name": "Estimert daglig fremgang" + }, + "lawn_area": { + "name": "Plenareal" + }, + "mowing_efficiency": { + "name": "Klippeeffektivitet" + }, + "rtk_map": { + "name": "RTK-kart" + }, + "rtk_trail_points": { + "name": "RTK-sporpunkter" + }, + "rtk_address": { + "name": "RTK-adresse" + }, + "rain_delay": { + "name": "Regnforsinkelse" + }, + "rain_remaining": { + "name": "Gjenværende regnforsinkelse" + }, + "battery_voltage": { + "name": "Batterispenning" + }, + "battery_temperature": { + "name": "Batteritemperatur" + }, + "battery_cycles_total": { + "name": "Batterisykluser (totalt)" + }, + "battery_cycles_since_reset": { + "name": "Batterisykluser siden tilbakestilling" + }, + "battery_cycles_reset_at": { + "name": "Siste tilbakestilling av batterisykluser" + }, + "blade_runtime_total": { + "name": "Knivenes driftstid (totalt)" + }, + "blade_runtime_current": { + "name": "Knivenes driftstid (nåværende)" + }, + "blade_runtime_reset_at": { + "name": "Siste tilbakestilling av knivtid" + }, + "mower_runtime_total": { + "name": "Total driftstid" + }, + "mower_home_time_total": { + "name": "Total tid ved basestasjonen" + }, + "mower_charging_time_total": { + "name": "Total ladetid" + }, + "mower_error_time_total": { + "name": "Total tid i feil" + }, + "maintenance_status": { + "name": "Vedlikeholdsstatus", + "state": { + "ok": "OK", + "blade_service_due": "Knivservice påkrevd", + "battery_service_due": "Batteriservice påkrevd" + } + }, + "pitch": { + "name": "Pitch" + }, + "roll": { + "name": "Roll" + }, + "yaw": { + "name": "Yaw" + }, + "last_update": { + "name": "Siste oppdatering" + }, + "last_update_age": { + "name": "Tid siden siste oppdatering" + } + }, + "binary_sensor": { + "online": { + "name": "Online" + }, + "iot_registered": { + "name": "IoT registrert" + }, + "mqtt_registered": { + "name": "MQTT registrert" + }, + "locked": { + "name": "Låst" + }, + "rain_triggered": { + "name": "Regn oppdaget" + }, + "robot_lifted": { + "name": "Robot løftet" + }, + "off_limits_enabled": { + "name": "Forbudte soner aktivert" + }, + "acs_enabled": { + "name": "ACS aktivert" + }, + "party_mode_enabled": { + "name": "Festmodus aktivert" + }, + "pause_mode_enabled": { + "name": "Pausemodus aktivert" + }, + "smart_edge_cut": { + "name": "Smart kantklipping" + }, + "save_hedgehogs": { + "name": "Redd pinnsvinene" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Automatisk fastvareoppdatering" + }, + "lock": { + "name": "Lås" + }, + "native_schedule": { + "name": "Innebygd tidsplan" + }, + "smart_edge_cut": { + "name": "Smart kantklipping" + }, + "save_hedgehogs": { + "name": "Redd pinnsvinene" + }, + "one_time_mowing_edge_cut": { + "name": "Kantklipping" + } + }, + "button": { + "refresh": { + "name": "Oppdater" + }, + "reset_blade_counter": { + "name": "Tilbakestill knivtid" + }, + "reset_battery_cycle_counter": { + "name": "Tilbakestill batterisykluser" + }, + "start_edge_cut": { + "name": "Start kantklipping" + }, + "start_one_time_mowing": { + "name": "Start engangsklipping" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Regnforsinkelse" + }, + "time_extension": { + "name": "Tidsforlengelse" + }, + "lawn_area": { + "name": "Plenareal" + }, + "lawn_perimeter": { + "name": "Plenomkrets" + }, + "one_time_mowing_runtime": { + "name": "Varighet for engangsklipping" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Klippesone" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "RTK-kart" + } + }, + "calendar": { + "schedule": { + "name": "Klippeplan" + } + }, + "device_tracker": { + "rtk_position": { + "name": "RTK-posisjon" + } + } + } +} diff --git a/custom_components/worx_vision_cloud/translations/pl.json b/custom_components/worx_vision_cloud/translations/pl.json index fdf657e..3d14644 100644 --- a/custom_components/worx_vision_cloud/translations/pl.json +++ b/custom_components/worx_vision_cloud/translations/pl.json @@ -24,20 +24,47 @@ } }, "entity": { - "lawn_mower": { - "mower": { - "name": "Kosiarka" - } - }, "sensor": { "battery_percent": { "name": "Bateria" }, "status": { - "name": "Status" + "name": "Status", + "state": { + "home": "w bazie", + "leaving_home": "wyjazd z bazy", + "going_home": "powrót do bazy", + "mowing": "koszenie", + "edge_cutting": "przycinanie krawędzi", + "charging": "ładowanie", + "paused": "pauza", + "idle": "bezczynna", + "manual_stop": "zatrzymana ręcznie", + "rain_delay": "opóźnienie po deszczu", + "locked": "zablokowana", + "error": "błąd", + "no_error": "brak błędu", + "offline": "offline" + } }, "error": { - "name": "Błąd" + "name": "Błąd", + "state": { + "home": "w bazie", + "leaving_home": "wyjazd z bazy", + "going_home": "powrót do bazy", + "mowing": "koszenie", + "edge_cutting": "przycinanie krawędzi", + "charging": "ładowanie", + "paused": "pauza", + "idle": "bezczynna", + "manual_stop": "zatrzymana ręcznie", + "rain_delay": "opóźnienie po deszczu", + "locked": "zablokowana", + "error": "błąd", + "no_error": "brak błędu", + "offline": "offline" + } }, "rssi": { "name": "RSSI" @@ -49,10 +76,25 @@ "name": "Harmonogram" }, "mowing_readiness": { - "name": "Gotowość do koszenia" + "name": "Gotowość do koszenia", + "state": { + "ready": "gotowa", + "mowing": "koszenie", + "charging": "ładowanie", + "battery_low": "niski poziom baterii", + "rain_delay": "opóźnienie po deszczu", + "error": "błąd", + "locked": "zablokowana", + "offline": "offline" + } }, "cloud_connection": { - "name": "Połączenie z chmurą" + "name": "Połączenie z chmurą", + "state": { + "ok": "Połączono", + "check": "Sprawdzanie", + "offline": "Offline" + } }, "api_capabilities": { "name": "Możliwości API" @@ -69,6 +111,18 @@ "area_mowed_today": { "name": "Skoszony obszar dzisiaj" }, + "next_schedule": { + "name": "Następny harmonogram" + }, + "area_mowed_total": { + "name": "Łącznie skoszona powierzchnia" + }, + "estimated_area_mowed_today": { + "name": "Szacowana powierzchnia skoszona dziś" + }, + "estimated_daily_progress": { + "name": "Szacowany postęp dzienny" + }, "lawn_area": { "name": "Powierzchnia trawnika" }, @@ -127,7 +181,12 @@ "name": "Czas błędów razem" }, "maintenance_status": { - "name": "Status serwisowy" + "name": "Status serwisowy", + "state": { + "ok": "OK", + "blade_service_due": "Wymiana ostrzy wymagana", + "battery_service_due": "Serwis baterii wymagany" + } }, "pitch": { "name": "Nachylenie X" @@ -256,6 +315,11 @@ "schedule": { "name": "Harmonogram koszenia" } + }, + "device_tracker": { + "rtk_position": { + "name": "Pozycja RTK" + } } } } diff --git a/custom_components/worx_vision_cloud/translations/sv.json b/custom_components/worx_vision_cloud/translations/sv.json new file mode 100644 index 0000000..77cdb20 --- /dev/null +++ b/custom_components/worx_vision_cloud/translations/sv.json @@ -0,0 +1,325 @@ +{ + "title": "Worx Vision Cloud PLUS", + "config": { + "step": { + "user": { + "title": "Worx Vision Cloud PLUS", + "description": "Använd samma inloggning som i Worx Landroid-appen.", + "data": { + "email": "E-post", + "password": "Lösenord", + "cloud": "Moln", + "verify_ssl": "Verifiera SSL", + "expose_raw_entities": "Exponera alla råa payload-fält som entiteter" + } + } + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till Worx Cloud.", + "invalid_auth": "Ogiltigt användarnamn eller lösenord.", + "rate_limited": "Gränsen för Worx Cloud-förfrågningar nådd. Försök igen senare." + }, + "abort": { + "already_configured": "Det här kontot är redan konfigurerat." + } + }, + "entity": { + "sensor": { + "battery_percent": { + "name": "Batteri" + }, + "status": { + "name": "Status", + "state": { + "home": "Vid basstationen", + "leaving_home": "Lämnar basstationen", + "going_home": "Återvänder till basstationen", + "mowing": "Klipper", + "edge_cutting": "Kantklippning", + "charging": "Laddar", + "paused": "Pausad", + "idle": "Inaktiv", + "manual_stop": "Manuellt stoppad", + "rain_delay": "Regnfördröjning", + "locked": "Låst", + "error": "Fel", + "no_error": "Inget fel", + "offline": "Offline" + } + }, + "error": { + "name": "Fel", + "state": { + "home": "Vid basstationen", + "leaving_home": "Lämnar basstationen", + "going_home": "Återvänder till basstationen", + "mowing": "Klipper", + "edge_cutting": "Kantklippning", + "charging": "Laddar", + "paused": "Pausad", + "idle": "Inaktiv", + "manual_stop": "Manuellt stoppad", + "rain_delay": "Regnfördröjning", + "locked": "Låst", + "error": "Fel", + "no_error": "Inget fel", + "offline": "Offline" + } + }, + "rssi": { + "name": "RSSI" + }, + "zone_current": { + "name": "Aktuell zon" + }, + "schedule": { + "name": "Schema" + }, + "mowing_readiness": { + "name": "Klippberedskap", + "state": { + "ready": "Redo", + "mowing": "Klipper", + "charging": "Laddar", + "battery_low": "Låg batterinivå", + "rain_delay": "Regnfördröjning", + "error": "Fel", + "locked": "Låst", + "offline": "Offline" + } + }, + "cloud_connection": { + "name": "Molnanslutning", + "state": { + "ok": "Ansluten", + "check": "Kontrollerar", + "offline": "Offline" + } + }, + "api_capabilities": { + "name": "API-funktioner" + }, + "push_notifications": { + "name": "Push-aviseringar" + }, + "daily_progress": { + "name": "Dagens förlopp" + }, + "remaining_progress": { + "name": "Kvar att klippa" + }, + "area_mowed_today": { + "name": "Klippt yta idag" + }, + "next_schedule": { + "name": "Nästa klippning" + }, + "area_mowed_total": { + "name": "Total klippt yta" + }, + "estimated_area_mowed_today": { + "name": "Uppskattad klippt yta idag" + }, + "estimated_daily_progress": { + "name": "Uppskattat dagligt förlopp" + }, + "lawn_area": { + "name": "Gräsmattans yta" + }, + "mowing_efficiency": { + "name": "Klippeffektivitet" + }, + "rtk_map": { + "name": "RTK-karta" + }, + "rtk_trail_points": { + "name": "RTK-spårpunkter" + }, + "rtk_address": { + "name": "RTK-adress" + }, + "rain_delay": { + "name": "Regnfördröjning" + }, + "rain_remaining": { + "name": "Återstående regnfördröjning" + }, + "battery_voltage": { + "name": "Batterispänning" + }, + "battery_temperature": { + "name": "Batteritemperatur" + }, + "battery_cycles_total": { + "name": "Battericykler (totalt)" + }, + "battery_cycles_since_reset": { + "name": "Battericykler sedan återställning" + }, + "battery_cycles_reset_at": { + "name": "Senaste återställning av battericykler" + }, + "blade_runtime_total": { + "name": "Knivarnas gångtid (totalt)" + }, + "blade_runtime_current": { + "name": "Knivarnas gångtid (aktuell)" + }, + "blade_runtime_reset_at": { + "name": "Senaste återställning av knivtid" + }, + "mower_runtime_total": { + "name": "Total gångtid" + }, + "mower_home_time_total": { + "name": "Total tid vid basstationen" + }, + "mower_charging_time_total": { + "name": "Total laddningstid" + }, + "mower_error_time_total": { + "name": "Total tid i fel" + }, + "maintenance_status": { + "name": "Underhållsstatus", + "state": { + "ok": "OK", + "blade_service_due": "Knivservice krävs", + "battery_service_due": "Batteriservice krävs" + } + }, + "pitch": { + "name": "Pitch" + }, + "roll": { + "name": "Roll" + }, + "yaw": { + "name": "Yaw" + }, + "last_update": { + "name": "Senaste uppdatering" + }, + "last_update_age": { + "name": "Tid sedan senaste uppdatering" + } + }, + "binary_sensor": { + "online": { + "name": "Online" + }, + "iot_registered": { + "name": "IoT registrerad" + }, + "mqtt_registered": { + "name": "MQTT registrerad" + }, + "locked": { + "name": "Låst" + }, + "rain_triggered": { + "name": "Regn upptäckt" + }, + "robot_lifted": { + "name": "Robot upplyft" + }, + "off_limits_enabled": { + "name": "Förbjudna zoner aktiverade" + }, + "acs_enabled": { + "name": "ACS aktiverat" + }, + "party_mode_enabled": { + "name": "Festläge aktiverat" + }, + "pause_mode_enabled": { + "name": "Pausläge aktiverat" + }, + "smart_edge_cut": { + "name": "Smart kantklippning" + }, + "save_hedgehogs": { + "name": "Rädda igelkottarna" + } + }, + "switch": { + "firmware_auto_upgrade": { + "name": "Automatisk firmware-uppdatering" + }, + "lock": { + "name": "Lås" + }, + "native_schedule": { + "name": "Inbyggt schema" + }, + "smart_edge_cut": { + "name": "Smart kantklippning" + }, + "save_hedgehogs": { + "name": "Rädda igelkottarna" + }, + "one_time_mowing_edge_cut": { + "name": "Kantklippning" + } + }, + "button": { + "refresh": { + "name": "Uppdatera" + }, + "reset_blade_counter": { + "name": "Återställ knivtid" + }, + "reset_battery_cycle_counter": { + "name": "Återställ battericykler" + }, + "start_edge_cut": { + "name": "Starta kantklippning" + }, + "start_one_time_mowing": { + "name": "Starta engångsklippning" + } + }, + "number": { + "rain_delay_minutes": { + "name": "Regnfördröjning" + }, + "time_extension": { + "name": "Tidsförlängning" + }, + "lawn_area": { + "name": "Gräsmattans yta" + }, + "lawn_perimeter": { + "name": "Gräsmattans omkrets" + }, + "one_time_mowing_runtime": { + "name": "Varaktighet för engångsklippning" + } + }, + "select": { + "one_time_mowing_zones": { + "name": "Klippzon" + } + }, + "update": { + "firmware": { + "name": "Firmware" + } + }, + "camera": { + "rtk_map_camera": { + "name": "RTK-karta" + } + }, + "calendar": { + "schedule": { + "name": "Klippschema" + } + }, + "device_tracker": { + "rtk_position": { + "name": "RTK-position" + } + } + } +}