Skip to content
Merged
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
402 changes: 398 additions & 4 deletions custom_components/narwal/__init__.py

Large diffs are not rendered by default.

21 changes: 13 additions & 8 deletions custom_components/narwal/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

from . import NarwalConfigEntry
from .const import (
CONF_MAP_ROTATION,
CONF_MAP_ZOOM,
CONF_SHOW_FURNITURE,
CONF_SHOW_FURNITURE_LABELS,
CONF_SHOW_ROOM_LABELS,
CONF_MAP_ROTATION,
CONF_MAP_ZOOM,
MAP_OPTION_DEFAULTS,
MAP_ROTATION_DEFAULT,
MAP_ZOOM_DEFAULT,
Expand Down Expand Up @@ -46,6 +46,15 @@
_DEBUG_TRAIL_MAX = _TRAIL_MAX_POINTS
_DEBUG_RECORD_INTERVAL = _TRAIL_RECORD_INTERVAL

_CLEANING_TRAIL_STATUSES = frozenset(
{
WorkingStatus.CLEANING_V2,
WorkingStatus.CLEANING,
WorkingStatus.CLEANING_ALT,
WorkingStatus.CUSTOM_CLEANING,
}
)


async def async_setup_entry(
hass: HomeAssistant,
Expand Down Expand Up @@ -280,12 +289,8 @@ def _handle_coordinator_update(self) -> None:

# Detect cleaning session transitions — clear trail on new session
current_status = state.working_status
was_cleaning = self._last_cleaning_status in (
WorkingStatus.CLEANING, WorkingStatus.CLEANING_ALT,
)
is_cleaning = current_status in (
WorkingStatus.CLEANING, WorkingStatus.CLEANING_ALT,
)
was_cleaning = self._last_cleaning_status in _CLEANING_TRAIL_STATUSES
is_cleaning = current_status in _CLEANING_TRAIL_STATUSES
if is_cleaning and not was_cleaning:
_LOGGER.info("New cleaning session — clearing trail and vision obstacles")
self._reset_trail()
Expand Down
45 changes: 40 additions & 5 deletions custom_components/narwal/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def configured_model_name(data: dict) -> str:
MAP_ZOOM_DEFAULT = 1.0 # renderer clamps to 1.0–2.0

CONF_DOCK_LIGHT_SUPPORTED = "dock_light_supported"
SERVICE_CLEAN_ROOMS = "clean_rooms"

def product_keys_for_model(label: str) -> set[str]:
"""Every product key known to belong to one selector model."""
Expand Down Expand Up @@ -144,12 +145,15 @@ def is_dock_light_supported(data: dict, options: dict | None = None) -> bool:
int(value): key for key, value in DOCK_LIGHT_MODES.items()
}

# HA fan_speed labels → FanLevel, from the app's user-visible suction names (sentence case, as HA shows fan_speed values directly). The enum members keep the app's internal identifiers, so DEEP surfaces as "Super" and SUPER as "Ultra".
# HA fan_speed labels -> FanLevel, from the app's user-visible suction names
# (sentence case, as HA shows fan_speed values directly). The enum members keep
# the app's internal identifiers, so DEEP surfaces as "Super Powerful" and
# SUPER as "Ultra".
_FAN_SPEED_CANONICAL: dict[str, FanLevel] = {
"Quiet": FanLevel.MUTE,
"Standard": FanLevel.NORMAL,
"Strong": FanLevel.STRONG,
"Super": FanLevel.DEEP,
"Super Powerful": FanLevel.DEEP,
"Ultra": FanLevel.SUPER,
}

Expand All @@ -173,10 +177,40 @@ def fan_speed_list_for(data: dict) -> list[str]:
return FAN_SPEED_LIST


def normalize_fan_level_for_model(data: dict, fan: FanLevel) -> FanLevel:
"""Return a persisted fan level supported by the configured model."""
if (
fan == FanLevel.SUPER
and data.get(CONF_PRODUCT_KEY) in NO_ULTRA_FAN_PRODUCT_KEYS
):
return FanLevel.DEEP
return fan


# FAN_SPEED_MAP also accepts the "… powerful" labels shipped through v1.0.3 and the
# original lowercase fan_speed values (quiet/normal/strong/max) so existing automations
# keep working; these aliases are not offered in FAN_SPEED_LIST.
# short "Super" label used by this stack, and the original lowercase fan_speed
# values (quiet/normal/strong/max) so existing automations keep working; these
# aliases are not offered in FAN_SPEED_LIST.
FAN_SPEED_MAP: dict[str, FanLevel] = _FAN_SPEED_CANONICAL | {
"Super": FanLevel.DEEP,
"Super powerful": FanLevel.DEEP,
"Ultra powerful": FanLevel.SUPER,
"quiet": FanLevel.MUTE,
"normal": FanLevel.NORMAL,
"strong": FanLevel.STRONG,
"max": FanLevel.SUPER,
}

# Meanings used by v1.0.5 and earlier persisted states. Keep this separate from
# current UI labels so a rename cannot silently change a restored robot enum.
UNVERSIONED_FAN_SPEED_MAP: dict[str, FanLevel] = {
"AI": FanLevel.UNSPECIFIED,
"Quiet": FanLevel.MUTE,
"Standard": FanLevel.NORMAL,
"Strong": FanLevel.STRONG,
"Super Powerful": FanLevel.DEEP,
"Ultra": FanLevel.SUPER,
"Super": FanLevel.DEEP,
"Super powerful": FanLevel.DEEP,
"Ultra powerful": FanLevel.SUPER,
"quiet": FanLevel.MUTE,
Expand Down Expand Up @@ -229,7 +263,8 @@ def fan_speed_list_for(data: dict) -> list[str]:
"https://help.narwal.com/helpcenter/vall/#/p2/question/all?eType=1&code={code}&lang=en-US"
)

# Select option id → robot enum. Option ids are rendered to the app's user-visible labels via translations.
# Select option id -> robot enum. Option ids are rendered to the app's
# user-visible labels via translations.
WORK_MODE_MAP: dict[str, WorkMode] = {
"vacuum": WorkMode.VACUUM,
"mop": WorkMode.MOP,
Expand Down
Loading
Loading