| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
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
| 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 | ||
| 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")]} | ||
| ) |
| Back | FazBrowse Home | New Git URL |
Uh oh!
There was an error while loading. Please reload this page.