-
-
Notifications
You must be signed in to change notification settings - Fork 239
Add consumables module for vacuums #1327
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
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
b4eebd9
Add vacuumconsumables
rytilahti bdd1daf
Dynamic consumable feature construction
rytilahti 3459000
Bind consumable on lambda def
rytilahti 36b30d3
Pass container to attribute_getter
rytilahti 995f91d
Use .get instead of getattr on the dict
rytilahti a64a54d
Use roll_brush_time instead of main_brush_lid_time for main brush
rytilahti 365bb3e
Add reset consumable
rytilahti 405fc50
Rename to consumables & cleanup code
rytilahti 1f1669f
Create getters for consumable information
rytilahti 2e3e9e7
Add tests
rytilahti 0785aac
Add consumables property to consumables module
sdb9696 b81b625
Merge remote-tracking branch 'upstream/master' into feat/vacuum_consu…
sdb9696 6488688
Add consumables CLI command
sdb9696 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| """Implementation of vacuum consumables.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from collections.abc import Mapping | ||
| from dataclasses import dataclass | ||
| from datetime import timedelta | ||
|
|
||
| from ...feature import Feature | ||
| from ..smartmodule import SmartModule | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @dataclass | ||
| class _ConsumableMeta: | ||
| """Consumable meta container.""" | ||
|
|
||
| #: Name of the consumable. | ||
| name: str | ||
| #: Internal id of the consumable | ||
| id: str | ||
| #: Data key in the device reported data | ||
| data_key: str | ||
| #: Lifetime | ||
| lifetime: timedelta | ||
|
|
||
|
|
||
| @dataclass | ||
| class Consumable: | ||
| """Consumable container.""" | ||
|
|
||
| #: Name of the consumable. | ||
| name: str | ||
| #: Id of the consumable | ||
| id: str | ||
| #: Lifetime | ||
| lifetime: timedelta | ||
| #: Used | ||
| used: timedelta | ||
| #: Remaining | ||
| remaining: timedelta | ||
| #: Device data key | ||
| _data_key: str | ||
|
|
||
|
|
||
| CONSUMABLE_METAS = [ | ||
| _ConsumableMeta( | ||
| "Main brush", | ||
| id="main_brush", | ||
| data_key="roll_brush_time", | ||
| lifetime=timedelta(hours=400), | ||
| ), | ||
| _ConsumableMeta( | ||
| "Side brush", | ||
| id="side_brush", | ||
| data_key="edge_brush_time", | ||
| lifetime=timedelta(hours=200), | ||
| ), | ||
| _ConsumableMeta( | ||
| "Filter", | ||
| id="filter", | ||
| data_key="filter_time", | ||
| lifetime=timedelta(hours=200), | ||
| ), | ||
| _ConsumableMeta( | ||
| "Sensor", | ||
| id="sensor", | ||
| data_key="sensor_time", | ||
| lifetime=timedelta(hours=30), | ||
| ), | ||
| _ConsumableMeta( | ||
| "Charging contacts", | ||
| id="charging_contacts", | ||
| data_key="charge_contact_time", | ||
| lifetime=timedelta(hours=30), | ||
| ), | ||
| # Unknown keys: main_brush_lid_time, rag_time | ||
| ] | ||
|
|
||
|
|
||
| class Consumables(SmartModule): | ||
| """Implementation of vacuum consumables.""" | ||
|
|
||
| REQUIRED_COMPONENT = "consumables" | ||
| QUERY_GETTER_NAME = "getConsumablesInfo" | ||
|
|
||
| _consumables: dict[str, Consumable] = {} | ||
|
|
||
| def _initialize_features(self) -> None: | ||
| """Initialize features.""" | ||
| for c_meta in CONSUMABLE_METAS: | ||
| if c_meta.data_key not in self.data: | ||
| continue | ||
|
|
||
| self._add_feature( | ||
| Feature( | ||
| self._device, | ||
| id=f"{c_meta.id}_used", | ||
| name=f"{c_meta.name} used", | ||
| container=self, | ||
| attribute_getter=lambda _, c_id=c_meta.id: self._consumables[ | ||
| c_id | ||
| ].used, | ||
| category=Feature.Category.Debug, | ||
| type=Feature.Type.Sensor, | ||
| ) | ||
| ) | ||
|
|
||
| self._add_feature( | ||
| Feature( | ||
| self._device, | ||
| id=f"{c_meta.id}_remaining", | ||
| name=f"{c_meta.name} remaining", | ||
| container=self, | ||
| attribute_getter=lambda _, c_id=c_meta.id: self._consumables[ | ||
| c_id | ||
| ].remaining, | ||
| category=Feature.Category.Info, | ||
| type=Feature.Type.Sensor, | ||
| ) | ||
| ) | ||
|
|
||
| self._add_feature( | ||
| Feature( | ||
| self._device, | ||
| id=f"{c_meta.id}_reset", | ||
| name=f"Reset {c_meta.name.lower()} consumable", | ||
| container=self, | ||
| attribute_setter=lambda c_id=c_meta.id: self.reset_consumable(c_id), | ||
| category=Feature.Category.Debug, | ||
| type=Feature.Type.Action, | ||
| ) | ||
| ) | ||
|
|
||
| async def _post_update_hook(self) -> None: | ||
| """Update the consumables.""" | ||
| if not self._consumables: | ||
| for consumable_meta in CONSUMABLE_METAS: | ||
| if consumable_meta.data_key not in self.data: | ||
| continue | ||
| used = timedelta(minutes=self.data[consumable_meta.data_key]) | ||
| consumable = Consumable( | ||
| id=consumable_meta.id, | ||
| name=consumable_meta.name, | ||
| lifetime=consumable_meta.lifetime, | ||
| used=used, | ||
| remaining=consumable_meta.lifetime - used, | ||
| _data_key=consumable_meta.data_key, | ||
| ) | ||
| self._consumables[consumable_meta.id] = consumable | ||
| else: | ||
| for consumable in self._consumables.values(): | ||
| consumable.used = timedelta(minutes=self.data[consumable._data_key]) | ||
| consumable.remaining = consumable.lifetime - consumable.used | ||
|
|
||
| async def reset_consumable(self, consumable_id: str) -> dict: | ||
| """Reset consumable stats.""" | ||
| consumable_name = self._consumables[consumable_id]._data_key.removesuffix( | ||
| "_time" | ||
| ) | ||
| return await self.call( | ||
| "resetConsumablesTime", {"reset_list": [consumable_name]} | ||
| ) | ||
|
|
||
| @property | ||
| def consumables(self) -> Mapping[str, Consumable]: | ||
| """Get list of consumables on the device.""" | ||
| return self._consumables | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from datetime import timedelta | ||
|
|
||
| import pytest | ||
| from pytest_mock import MockerFixture | ||
|
|
||
| from kasa import Module | ||
| from kasa.smart import SmartDevice | ||
| from kasa.smart.modules.consumables import CONSUMABLE_METAS | ||
|
|
||
| from ...device_fixtures import get_parent_and_child_modules, parametrize | ||
|
|
||
| consumables = parametrize( | ||
| "has consumables", component_filter="consumables", protocol_filter={"SMART"} | ||
| ) | ||
|
|
||
|
|
||
| @consumables | ||
| @pytest.mark.parametrize( | ||
| "consumable_name", [consumable.id for consumable in CONSUMABLE_METAS] | ||
| ) | ||
| @pytest.mark.parametrize("postfix", ["used", "remaining"]) | ||
| async def test_features(dev: SmartDevice, consumable_name: str, postfix: str): | ||
| """Test that features are registered and work as expected.""" | ||
| consumables = next(get_parent_and_child_modules(dev, Module.Consumables)) | ||
| assert consumables is not None | ||
|
|
||
| feature_name = f"{consumable_name}_{postfix}" | ||
|
|
||
| feat = consumables._device.features[feature_name] | ||
| assert isinstance(feat.value, timedelta) | ||
|
|
||
|
|
||
| @consumables | ||
| @pytest.mark.parametrize( | ||
| ("consumable_name", "data_key"), | ||
| [(consumable.id, consumable.data_key) for consumable in CONSUMABLE_METAS], | ||
| ) | ||
| async def test_erase( | ||
| dev: SmartDevice, mocker: MockerFixture, consumable_name: str, data_key: str | ||
| ): | ||
| """Test autocollection switch.""" | ||
| consumables = next(get_parent_and_child_modules(dev, Module.Consumables)) | ||
| call = mocker.spy(consumables, "call") | ||
|
|
||
| feature_name = f"{consumable_name}_reset" | ||
| feat = dev._features[feature_name] | ||
| await feat.set_value(True) | ||
|
|
||
| call.assert_called_with( | ||
| "resetConsumablesTime", {"reset_list": [data_key.removesuffix("_time")]} | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.