diff --git a/kasa/module.py b/kasa/module.py index 5066c953..854ab960 100644 --- a/kasa/module.py +++ b/kasa/module.py @@ -37,7 +37,11 @@ class Module(ABC): def _add_feature(self, feature: Feature): """Add module feature.""" - feat_name = f"{self._module}_{feature.name}" + + def _slugified_name(name): + return name.lower().replace(" ", "_").replace("'", "_") + + feat_name = _slugified_name(feature.name) if feat_name in self._module_features: raise KasaException("Duplicate name detected %s" % feat_name) self._module_features[feat_name] = feature diff --git a/kasa/smart/modules/temperature.py b/kasa/smart/modules/temperature.py index c33e565b..dbfe7c63 100644 --- a/kasa/smart/modules/temperature.py +++ b/kasa/smart/modules/temperature.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: class TemperatureSensor(SmartModule): """Implementation of temperature module.""" - REQUIRED_COMPONENT = "humidity" + REQUIRED_COMPONENT = "temperature" QUERY_GETTER_NAME = "get_comfort_temp_config" def __init__(self, device: "SmartDevice", module: str): @@ -53,7 +53,7 @@ class TemperatureSensor(SmartModule): @property def temperature_warning(self) -> bool: - """Return True if humidity is outside of the wanted range.""" + """Return True if temperature is outside of the wanted range.""" return self._device.sys_info["current_temp_exception"] != 0 @property diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py index 0917f081..bec48bde 100644 --- a/kasa/tests/conftest.py +++ b/kasa/tests/conftest.py @@ -2,7 +2,7 @@ import warnings from typing import Dict from unittest.mock import MagicMock -import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/3342 +import pytest from kasa import ( DeviceConfig, diff --git a/kasa/tests/device_fixtures.py b/kasa/tests/device_fixtures.py index 085bab8e..71cc34bd 100644 --- a/kasa/tests/device_fixtures.py +++ b/kasa/tests/device_fixtures.py @@ -1,3 +1,4 @@ +from itertools import chain from typing import Dict, List, Set import pytest @@ -106,6 +107,7 @@ DIMMERS = { } HUBS_SMART = {"H100"} +SENSORS_SMART = {"T315"} WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT} WITH_EMETER_SMART = {"P110", "KP125M", "EP25"} @@ -121,6 +123,7 @@ ALL_DEVICES_SMART = ( .union(STRIPS_SMART) .union(DIMMERS_SMART) .union(HUBS_SMART) + .union(SENSORS_SMART) .union(SWITCHES_SMART) ) ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART) @@ -263,6 +266,9 @@ dimmers_smart = parametrize( hubs_smart = parametrize( "hubs smart", model_filter=HUBS_SMART, protocol_filter={"SMART"} ) +sensors_smart = parametrize( + "sensors smart", model_filter=SENSORS_SMART, protocol_filter={"SMART.CHILD"} +) device_smart = parametrize( "devices smart", model_filter=ALL_DEVICES_SMART, protocol_filter={"SMART"} ) @@ -283,6 +289,7 @@ def check_categories(): + bulb_smart.args[1] + dimmers_smart.args[1] + hubs_smart.args[1] + + sensors_smart.args[1] ) diffs: Set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures) if diffs: @@ -299,24 +306,14 @@ check_categories() def device_for_fixture_name(model, protocol): if "SMART" in protocol: - for d in PLUGS_SMART: + for d in chain( + PLUGS_SMART, SWITCHES_SMART, STRIPS_SMART, HUBS_SMART, SENSORS_SMART + ): if d in model: return SmartDevice - for d in SWITCHES_SMART: - if d in model: - return SmartDevice - for d in BULBS_SMART: + for d in chain(BULBS_SMART, DIMMERS_SMART): if d in model: return SmartBulb - for d in DIMMERS_SMART: - if d in model: - return SmartBulb - for d in STRIPS_SMART: - if d in model: - return SmartDevice - for d in HUBS_SMART: - if d in model: - return SmartDevice else: for d in STRIPS_IOT: if d in model: @@ -378,7 +375,8 @@ async def get_device_for_fixture(fixture_data: FixtureInfo): discovery_data = { "system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]} } - if discovery_data: # Child devices do not have discovery info + + if discovery_data: # Child devices do not have discovery info d.update_from_discover_info(discovery_data) await _update_and_close(d) @@ -392,7 +390,7 @@ async def get_device_for_fixture_protocol(fixture, protocol): return await get_device_for_fixture(fixture_info) -@pytest.fixture(params=FIXTURE_DATA, ids=idgenerator) +@pytest.fixture(params=filter_fixtures("main devices"), ids=idgenerator) async def dev(request): """Device fixture. diff --git a/kasa/tests/fixtureinfo.py b/kasa/tests/fixtureinfo.py index 70d385f6..08414ad4 100644 --- a/kasa/tests/fixtureinfo.py +++ b/kasa/tests/fixtureinfo.py @@ -93,7 +93,7 @@ def filter_fixtures( data_root_filter: return fixtures containing the supplied top level key, i.e. discovery_result - protocol_filter: set of protocols to match, IOT or SMART + protocol_filter: set of protocols to match, IOT, SMART, SMART.CHILD model_filter: set of device models to match component_filter: filter SMART fixtures that have the provided component in component_nego details. diff --git a/kasa/tests/fixtures/smart/child/.gitkeep b/kasa/tests/fixtures/smart/child/.gitkeep deleted file mode 100644 index 74bef849..00000000 --- a/kasa/tests/fixtures/smart/child/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -Can be deleted when first fixture is added diff --git a/kasa/tests/fixtures/smart/child/T315(EU)_1.0_1.7.0.json b/kasa/tests/fixtures/smart/child/T315(EU)_1.0_1.7.0.json new file mode 100644 index 00000000..4fc49b0e --- /dev/null +++ b/kasa/tests/fixtures/smart/child/T315(EU)_1.0_1.7.0.json @@ -0,0 +1,537 @@ +{ + "component_nego" : { + "component_list" : [ + { + "id" : "device", + "ver_code" : 2 + }, + { + "id" : "quick_setup", + "ver_code" : 3 + }, + { + "id" : "trigger_log", + "ver_code" : 1 + }, + { + "id" : "time", + "ver_code" : 1 + }, + { + "id" : "device_local_time", + "ver_code" : 1 + }, + { + "id" : "account", + "ver_code" : 1 + }, + { + "id" : "synchronize", + "ver_code" : 1 + }, + { + "id" : "cloud_connect", + "ver_code" : 1 + }, + { + "id" : "iot_cloud", + "ver_code" : 1 + }, + { + "id" : "firmware", + "ver_code" : 1 + }, + { + "id" : "localSmart", + "ver_code" : 1 + }, + { + "id" : "battery_detect", + "ver_code" : 1 + }, + { + "id" : "temperature", + "ver_code" : 1 + }, + { + "id" : "humidity", + "ver_code" : 1 + }, + { + "id" : "temp_humidity_record", + "ver_code" : 1 + }, + { + "id" : "comfort_temperature", + "ver_code" : 1 + }, + { + "id" : "comfort_humidity", + "ver_code" : 1 + }, + { + "id" : "report_mode", + "ver_code" : 1 + } + ] + }, + "get_connect_cloud_state" : { + "status" : 0 + }, + "get_device_info" : { + "at_low_battery" : false, + "avatar" : "", + "battery_percentage" : 100, + "bind_count" : 1, + "category" : "subg.trigger.temp-hmdt-sensor", + "current_humidity" : 61, + "current_humidity_exception" : 1, + "current_temp" : 21.4, + "current_temp_exception" : 0, + "device_id" : "SCRUBBED_CHILD_DEVICE_ID_1", + "fw_ver" : "1.7.0 Build 230424 Rel.170332", + "hw_id" : "00000000000000000000000000000000", + "hw_ver" : "1.0", + "jamming_rssi" : -122, + "jamming_signal_level" : 1, + "lastOnboardingTimestamp" : 1706990901, + "mac" : "F0A731000000", + "model" : "T315", + "nickname" : "I01BU0tFRF9OQU1FIw==", + "oem_id" : "00000000000000000000000000000000", + "parent_device_id" : "0000000000000000000000000000000000000000", + "region" : "Europe/Berlin", + "report_interval" : 16, + "rssi" : -56, + "signal_level" : 3, + "specs" : "EU", + "status" : "online", + "status_follow_edge" : false, + "temp_unit" : "celsius", + "type" : "SMART.TAPOSENSOR" + }, + "get_fw_download_state" : { + "cloud_cache_seconds" : 1, + "download_progress" : 0, + "reboot_time" : 5, + "status" : 0, + "upgrade_time" : 5 + }, + "get_latest_fw" : { + "fw_ver" : "1.8.0 Build 230921 Rel.091446", + "hw_id" : "00000000000000000000000000000000", + "need_to_upgrade" : true, + "oem_id" : "00000000000000000000000000000000", + "release_date" : "2023-12-01", + "release_note" : "Modifications and Bug Fixes:\nEnhance the stability of the sensor.", + "type" : 2 + }, + "get_temp_humidity_records" : { + "local_time" : 1709061516, + "past24h_humidity" : [ + 60, + 60, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 58, + 59, + 59, + 58, + 59, + 59, + 59, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 59, + 59, + 59, + 59, + 59, + 59, + 60, + 60, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 59, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 64, + 56, + 53, + 55, + 56, + 57, + 57, + 58, + 59, + 63, + 63, + 62, + 62, + 62, + 62, + 61, + 62, + 62, + 61, + 61 + ], + "past24h_humidity_exception" : [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 3, + 2, + 2, + 2, + 2, + 1, + 2, + 2, + 1, + 1 + ], + "past24h_temp" : [ + 217, + 216, + 215, + 214, + 214, + 214, + 214, + 214, + 214, + 213, + 213, + 213, + 213, + 213, + 212, + 212, + 211, + 211, + 211, + 211, + 211, + 211, + 212, + 212, + 212, + 211, + 211, + 211, + 211, + 212, + 212, + 212, + 212, + 212, + 211, + 211, + 211, + 212, + 213, + 214, + 214, + 214, + 213, + 212, + 212, + 212, + 212, + 212, + 212, + 212, + 212, + 212, + 212, + 213, + 213, + 213, + 213, + 213, + 213, + 213, + 213, + 213, + 213, + 214, + 214, + 215, + 215, + 215, + 214, + 215, + 216, + 216, + 216, + 216, + 216, + 216, + 216, + 205, + 196, + 210, + 213, + 213, + 213, + 213, + 213, + 214, + 215, + 214, + 214, + 213, + 213, + 214, + 214, + 214, + 213, + 213 + ], + "past24h_temp_exception" : [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "temp_unit" : "celsius" + }, + "get_trigger_logs" : { + "logs" : [ + { + "event" : "tooDry", + "eventId" : "118040a8-5422-1100-0804-0a8542211000", + "id" : 1, + "timestamp" : 1706996915 + } + ], + "start_id" : 1, + "sum" : 1 + } +} diff --git a/kasa/tests/smart/__init__.py b/kasa/tests/smart/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kasa/tests/smart/modules/__init__.py b/kasa/tests/smart/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kasa/tests/smart/modules/test_humidity.py b/kasa/tests/smart/modules/test_humidity.py new file mode 100644 index 00000000..99e4702e --- /dev/null +++ b/kasa/tests/smart/modules/test_humidity.py @@ -0,0 +1,26 @@ +import pytest + +from kasa.smart.modules import HumiditySensor +from kasa.tests.device_fixtures import parametrize + +humidity = parametrize("has humidity", component_filter="humidity", protocol_filter={"SMART.CHILD"}) + + +@humidity +@pytest.mark.parametrize( + "feature, type", + [ + ("humidity", int), + ("humidity_warning", bool), + ], +) +async def test_humidity_features(dev, feature, type): + """Test that features are registered and work as expected.""" + humidity: HumiditySensor = dev.modules["HumiditySensor"] + + prop = getattr(humidity, feature) + assert isinstance(prop, type) + + feat = humidity._module_features[feature] + assert feat.value == prop + assert isinstance(feat.value, type) diff --git a/kasa/tests/smart/modules/test_temperature.py b/kasa/tests/smart/modules/test_temperature.py new file mode 100644 index 00000000..649b5bc4 --- /dev/null +++ b/kasa/tests/smart/modules/test_temperature.py @@ -0,0 +1,27 @@ +import pytest + +from kasa.smart.modules import TemperatureSensor +from kasa.tests.device_fixtures import parametrize + +temperature = parametrize("has temperature", component_filter="temperature", protocol_filter={"SMART.CHILD"}) + + +@temperature +@pytest.mark.parametrize( + "feature, type", + [ + ("temperature", float), + ("temperature_warning", bool), + ("temperature_unit", str), + ], +) +async def test_temperature_features(dev, feature, type): + """Test that features are registered and work as expected.""" + temp_module: TemperatureSensor = dev.modules["TemperatureSensor"] + + prop = getattr(temp_module, feature) + assert isinstance(prop, type) + + feat = temp_module._module_features[feature] + assert feat.value == prop + assert isinstance(feat.value, type)