Skip to content

Add a trait for controlling the dock mop dryer #905

Description

@dschuld

Summary

There's currently no way to start or stop a mop drying cycle through the trait API. dry_status is readable, it was annotated with dps metadata in #740 and is exposed to Home Assistant via is_field_supported(StatusV2, StatusField.DRY_STATUS). But the write side has no trait behind it, so consumers have to fall back to RoborockCommand.APP_SET_DRYER_STATUS.

I want to contribute a function to Home Assistant to switch the dryer on and off , but this is currently only possible via this workaround that I do not think fits into the design. Other switches in the Home Assistant integration are now entirely trait-driven (child_lock, flow_led_status, dnd, valley_electricity_timer).

Prior Art

#262 asked for exactly this in Dec 2024:

correctly sets the mop dry time, however does not turn on the drying. Is this intended, as there seems to be no way to programmatically start the drying process?

It was closed by the reporter with:

Hi, solved with app_set_dryer_status with param status:1

So the command is confirmed working by another user, but the finding only ever lived in a closed issue and it was never carried into the library. APP_SET_DRYER_SETTING configures the dry duration; APP_SET_DRYER_STATUS starts and stops the cycle. I've since confirmed the latter against my own dock.

That issue predates the trait system entirely, so as far as I can tell this was never considered and rejected.

Proposed fix

A switch trait in the shape of the existing ones (ChildLockTrait, ValleyElectricityTimerTrait), gated on the dock being able to dry:

class MopDryerTrait(common.V1TraitMixin, common.RoborockSwitchBase):
    """Trait for controlling the dock mop dryer."""

    requires_dock_features = lambda f: f.is_dryable

    def __init__(self, status_trait: StatusTrait) -> None:
        super().__init__()
        self._status_trait = status_trait

    async def refresh(self) -> None:
        await self._status_trait.refresh()

    @property
    def is_on(self) -> bool:
        return bool(self._status_trait.dry_status)

    async def enable(self) -> None:
        await self.rpc_channel.send_command(
            RoborockCommand.APP_SET_DRYER_STATUS, params={"status": 1}
        )
        self._status_trait.dry_status = 1  # optimistic, as in the other switch traits

    async def disable(self) -> None:
        await self.rpc_channel.send_command(
            RoborockCommand.APP_SET_DRYER_STATUS, params={"status": 0}
        )
        self._status_trait.dry_status = 0

Because it takes a constructor argument it would be created in the special-case block in discover_features(), alongside WashTowelModeTrait and ObstaclePhotoTrait.

This follows the same pattern as the action methods being added to V1 traits elsewhere (#856, and the q7 series in #841#844).

TBC before opening a PR

1. Where should the state come from?

Unlike every other switch trait, this one has no query command of its own. If a cycle is running is reported as dry_status on StatusV2. The sketch above therefore holds the status trait and refreshes through it, which keeps V1TraitMixin's command/converter unused.

The alternative is a self-contained trait backed by APP_GET_DRYER_SETTING with a new DryerSetting dataclass. I didn't go that way because I have no sample of that response and didn't want to guess at the format, But I'm happy to capture it from my dock if you'd prefer that shape, it would also make the dry-duration setting available.

2. Which capability should be used to check?

The code uses dock_features.is_dryable. The alternatives are is_supported_drying (device feature bit) or is_field_supported(StatusV2, StatusField.DRY_STATUS), which is what the binary sensor on the HA side already uses.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions