Skip to content

Refactor Mqtt Manager and Roborock Device #364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Refactor Mqtt Manager and Roborock Device #364

wants to merge 11 commits into from

Conversation

Lash-L
Copy link
Collaborator

@Lash-L Lash-L commented Apr 9, 2025

No description provided.

from .containers import Consumable, DeviceFeatures, DnDTimer, RoborockBase


@dataclass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The general shape looks good with the base class that supports listeners and handling updates.

I'm wondering if we can merge the trait and the dataclass objects. The API right now is like this:

if device.dnd:
    print(device.dnd.status.enabled)

But perhaps that extra level of status is worth trying to avoid? Then it could look like this:

if device.dnd:
    print(device.dnd.enabled)

The down side of this is that the dataclass has to become mutable, and we will likely want more subclassing (e.g. There is already RoborockBase for handling parsing of objects, but should it also have listeners? or make another class hierarchy? just want to avoid multiple paths to inherit RoborockBase).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I just pushed a change that attempts to do this. It's a bit weird with the to and from dict logic. I started wondering though if the logic we use right now is overkill. We do all sorts of camelization and decamelization, but maybe it is better to just be explicit. It is less 'engineered'/ less fancy. But it is probably less prone to errors? I didn't touch the support features stuff you talked about. But want to take a look at that in the diff and see what you think about it/ if you have other feedback? We'd basically remove containers for anything that is a device trait.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still can find ways to simplify this, but not yet seeing it. Maybe it's but cutting some of the requirements for now that are complex, then adding them in a follow up? The challenge is to have a nice API you like, then figure out how to add workarounds to add in extra constraints w/o uglifying the API.

Can we cut requirements for now that every trait has its own subscriber? I feel like that adds a little complexity i think we can work up to.

How about reducing the number of states a trait can be in? Right now we have these states to consider:

  • Can be None (not supported)
  • Can be present, but all fields are None (supported but not populated yet yet)
  • Can be present with a status (support but failed population on first message?)
  • Can be present with some fields None and some not None (does it happen? maybe depends on device? unclear to caller)

I think we need to remove 2 of these if we can... e.g. present with all None fields seems like it could be nice to avoid if its not meaningful.

Parsing wise, it feels like Dnd should be a @dataclass given all that initialization. Thee are also parsing libraries we can use e.g. mashumaro

Copy link
Collaborator Author

@Lash-L Lash-L Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will responding to the rest when I'm at my machine. As far as serialization goes. We used dacite in the past and did camelization of atttibutes as sometimes they would randomly throw in a snake case attribute. This added some complexity, and when I removed the dacite dependency, I just wrote my own serializer as there were a few things that were happening that didn't work well with the libraries I tried(including madhumaru). Perhaps while we are already making a breaking change, we can switch to a library that may handle things slightly differently.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe:

The "update" method can actually be a class method and return a instance of the trait.

Before we get any data it is None.

So long as it is supported, when we get data we update it.

self.dnd = DndTrait.update(data)

Not sure if that potentially opens the door to async problems.

Thoughts @allenporter

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it gets a bit trickier with something like Consumable. Where different devices have different attributes on them. and therefore have to have some None attributes

super().__init__(send_command)

@classmethod
def supported(cls, features: DeviceFeatures) -> bool:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now the logic for this mapping is spread across a few places, the device features object and the trait itself, but wondering if it can live in a single place.

I think it either needs to be:

  • in the device features code, directly point to the trait, then look up the reference when building the device. i'm feeling like the first option may be simplest, though it flips around the dependencies it may have to move if going that way.
  • Define an enums the trait can reference then the parsing code can look it up in the right place. This may be a challenge since there are multiple ways to determine if something is supported.
  • push the "is supported" logic only in the trait (and figure out the right apis to pass the needed data through when building the device). This may be a challenge since there are multiple ways to determine if something is supported.

return trait_instance
return None

async def connect(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you have a TODO above, but i think it would be best to just now pass in a connected session and not add this method, or add a TODO remove here i guess.


def determine_supported_traits(self, trait: type[DeviceTrait]):
def _send_command(
method: RoborockCommand | str, params: list | dict | int | None = None, use_cloud: bool = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we omit use_cloud if we're not using it yet?

"""Represents the features supported by a Roborock device."""

# Features derived from robot_new_features
is_map_carpet_add_support: bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add the details about the bits into the metadata for the field itself like this:

from dataclasses import dataclass, field
from typing import Any

@dataclass
class Example:
    is_map_carpet_add_support: bool = field(metadata={'feature': '1073741824'})

You can use fields() to iterate over all the fields and access the .metadata to get the dict. The field could be named different things for different feature types e.g. ("upper_32_bits": or "feature_bit_str":) and change the logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that's a cool idea. i like it

from .containers import Consumable, DeviceFeatures, DnDTimer, RoborockBase


@dataclass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still can find ways to simplify this, but not yet seeing it. Maybe it's but cutting some of the requirements for now that are complex, then adding them in a follow up? The challenge is to have a nice API you like, then figure out how to add workarounds to add in extra constraints w/o uglifying the API.

Can we cut requirements for now that every trait has its own subscriber? I feel like that adds a little complexity i think we can work up to.

How about reducing the number of states a trait can be in? Right now we have these states to consider:

  • Can be None (not supported)
  • Can be present, but all fields are None (supported but not populated yet yet)
  • Can be present with a status (support but failed population on first message?)
  • Can be present with some fields None and some not None (does it happen? maybe depends on device? unclear to caller)

I think we need to remove 2 of these if we can... e.g. present with all None fields seems like it could be nice to avoid if its not meaningful.

Parsing wise, it feels like Dnd should be a @dataclass given all that initialization. Thee are also parsing libraries we can use e.g. mashumaro

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants