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
42 changes: 37 additions & 5 deletions custom_components/narwal/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from homeassistant.const import Platform

from .narwal_client import FanLevel
from .narwal_client import FanLevel, MopHumidity, MopStrengthLevel, WorkMode

DOMAIN = "narwal"
DEFAULT_PORT = 9002
Expand All @@ -29,13 +29,45 @@
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.CAMERA,
Platform.SELECT,
Platform.NUMBER,
]

FAN_SPEED_MAP: dict[str, FanLevel] = {
"quiet": FanLevel.QUIET,
# HA fan_speed labels → FanLevel, verbatim 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 powerful".
_FAN_SPEED_CANONICAL: dict[str, FanLevel] = {
"Quiet": FanLevel.MUTE,
"Standard": FanLevel.NORMAL,
"Strong": FanLevel.STRONG,
"Super powerful": FanLevel.DEEP,
"Ultra powerful": FanLevel.SUPER,
}

FAN_SPEED_LIST: list[str] = list(_FAN_SPEED_CANONICAL)

# FAN_SPEED_MAP also accepts 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 | {
"quiet": FanLevel.MUTE,
"normal": FanLevel.NORMAL,
"strong": FanLevel.STRONG,
"max": FanLevel.MAX,
"max": FanLevel.SUPER,
}

# 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,
"vacuum_then_mop": WorkMode.VACUUM_THEN_MOP,
"vacuum_and_mop": WorkMode.VACUUM_AND_MOP,
}
WATER_MAP: dict[str, MopHumidity] = {
"dry": MopHumidity.DRY,
"normal": MopHumidity.NORMAL,
"wet": MopHumidity.WET,
}
MOP_STRENGTH_MAP: dict[str, MopStrengthLevel] = {
"normal": MopStrengthLevel.NORMAL,
"high": MopStrengthLevel.HIGH,
}

FAN_SPEED_LIST: list[str] = list(FAN_SPEED_MAP.keys())
PASSES_MIN = 1
PASSES_MAX = 3
26 changes: 25 additions & 1 deletion custom_components/narwal/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@
import asyncio
import logging
import time
from dataclasses import dataclass
from datetime import timedelta

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .narwal_client import NarwalClient, NarwalConnectionError, NarwalState
from .narwal_client import (
WorkMode,
FanLevel,
MopHumidity,
MopStrengthLevel,
NarwalClient,
NarwalConnectionError,
NarwalState,
)
from .narwal_client.const import WorkingStatus

from .const import DOMAIN
Expand All @@ -25,6 +34,20 @@
FAST_POLL_MAX = 6 # up to 60s of fast polling before falling back to normal


@dataclass
class CleanSettings:
"""User-selected clean parameters, applied at the next room clean start.

Single source of truth the select/number entities mutate and the clean-start path reads; each entity persists its value via RestoreEntity, so they survive restarts. Only fan and water also have live setters — work_mode/mop_strength/passes take effect at the next start.
"""

work_mode: WorkMode = WorkMode.VACUUM_AND_MOP
fan: FanLevel = FanLevel.NORMAL
water: MopHumidity = MopHumidity.NORMAL
mop_strength: MopStrengthLevel = MopStrengthLevel.NORMAL
passes: int = 1


class NarwalCoordinator(DataUpdateCoordinator[NarwalState]):
"""Push-mode coordinator for Narwal vacuum.

Expand Down Expand Up @@ -54,6 +77,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
device_id=entry.data.get("device_id", ""),
topic_prefix=topic_prefix,
)
self.clean_settings = CleanSettings()
self._listen_task: asyncio.Task[None] | None = None
self._fast_poll_remaining = 0
self._prev_working_status = WorkingStatus.UNKNOWN
Expand Down
4 changes: 3 additions & 1 deletion custom_components/narwal/narwal_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Narwal robot vacuum client library — local WebSocket API."""

from .client import NarwalClient, NarwalCommandError, NarwalConnectionError
from .const import CommandResult, FanLevel, MopHumidity, WorkingStatus
from .const import CommandResult, FanLevel, MopHumidity, MopStrengthLevel, WorkMode, WorkingStatus
from .models import CommandResponse, DeviceInfo, MapData, MapDisplayData, NarwalState, RoomInfo
from .protocol import build_frame, parse_frame

Expand All @@ -17,7 +17,9 @@
"MapData",
"MapDisplayData",
"MopHumidity",
"MopStrengthLevel",
"RoomInfo",
"WorkMode",
"WorkingStatus",
"build_frame",
"parse_frame",
Expand Down
Loading