Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/worx_vision_cloud/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ 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."""
Expand Down
2 changes: 1 addition & 1 deletion custom_components/worx_vision_cloud/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion custom_components/worx_vision_cloud/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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:
Expand Down
20 changes: 10 additions & 10 deletions custom_components/worx_vision_cloud/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@
}

SCHEDULE_DAY_LABELS = {
"monday": "pon",
"tuesday": "wt",
"wednesday": "sr",
"thursday": "czw",
"friday": "pt",
"saturday": "sob",
"sunday": "niedz",
"monday": "Mon",
"tuesday": "Tue",
"wednesday": "Wed",
"thursday": "Thu",
"friday": "Fri",
"saturday": "Sat",
"sunday": "Sun",
}

SCHEDULE_DAY_INDEX = {
Expand Down Expand Up @@ -393,20 +393,20 @@ def schedule_slot_summary(slot: Any) -> str:
text = day or "slot"

if get_dict_value(slot, "boundary"):
text = f"{text} + krawedz"
text = f"{text} + edge"
return text


def schedule_summary(device: Any) -> str | None:
"""Return a compact schedule summary for Home Assistant state."""
slots = schedule_slots(device)
if not slots:
return "brak aktywnych slotow"
return "no active slots"

summary = ", ".join(schedule_slot_summary(slot) for slot in slots)
if len(summary) <= MAX_STRING_STATE_LENGTH:
return summary
return f"{len(slots)} aktywnych slotow"
return f"{len(slots)} active slots"


def schedule_attributes(device: Any) -> dict[str, Any]:
Expand Down
68 changes: 54 additions & 14 deletions custom_components/worx_vision_cloud/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand All @@ -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(
Expand All @@ -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]:
Expand All @@ -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:
Expand Down
108 changes: 72 additions & 36 deletions custom_components/worx_vision_cloud/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,47 +58,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):
Expand All @@ -123,16 +147,18 @@ 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):
return get_dict_value(getattr(device, "error", {}), key, default)


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:
Expand Down Expand Up @@ -394,7 +420,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]:
Expand Down Expand Up @@ -587,6 +613,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,
),
Expand All @@ -595,6 +623,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"),
Expand Down Expand Up @@ -633,6 +663,8 @@ def _rtk_address_attributes(
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,
),
Expand All @@ -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,
),
Expand Down Expand Up @@ -872,6 +906,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,
),
Expand Down
Loading