Merge remote-tracking branch 'upstream/master' into feat/device_update

This commit is contained in:
sdb9696
2024-05-14 18:35:23 +01:00
83 changed files with 3312 additions and 933 deletions

View File

@@ -109,7 +109,7 @@ DIMMERS = {
}
HUBS_SMART = {"H100", "KH100"}
SENSORS_SMART = {"T310", "T315", "T300"}
SENSORS_SMART = {"T310", "T315", "T300", "T110"}
THERMOSTATS_SMART = {"KE100"}
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
@@ -203,14 +203,14 @@ wallswitch_iot = parametrize(
"wall switches iot", model_filter=SWITCHES, protocol_filter={"IOT"}
)
strip = parametrize("strips", model_filter=STRIPS, protocol_filter={"SMART", "IOT"})
dimmer = parametrize("dimmers", model_filter=DIMMERS, protocol_filter={"IOT"})
lightstrip = parametrize(
dimmer_iot = parametrize("dimmers", model_filter=DIMMERS, protocol_filter={"IOT"})
lightstrip_iot = parametrize(
"lightstrips", model_filter=LIGHT_STRIPS, protocol_filter={"IOT"}
)
# bulb types
dimmable = parametrize("dimmable", model_filter=DIMMABLE, protocol_filter={"IOT"})
non_dimmable = parametrize(
dimmable_iot = parametrize("dimmable", model_filter=DIMMABLE, protocol_filter={"IOT"})
non_dimmable_iot = parametrize(
"non-dimmable", model_filter=BULBS - DIMMABLE, protocol_filter={"IOT"}
)
variable_temp = parametrize(
@@ -292,12 +292,12 @@ device_iot = parametrize(
def check_categories():
"""Check that every fixture file is categorized."""
categorized_fixtures = set(
dimmer.args[1]
dimmer_iot.args[1]
+ strip.args[1]
+ plug.args[1]
+ bulb.args[1]
+ wallswitch.args[1]
+ lightstrip.args[1]
+ lightstrip_iot.args[1]
+ bulb_smart.args[1]
+ dimmers_smart.args[1]
+ hubs_smart.args[1]

View File

@@ -189,6 +189,11 @@ class FakeSmartTransport(BaseTransport):
if "current_rule_id" in info["get_dynamic_light_effect_rules"]:
del info["get_dynamic_light_effect_rules"]["current_rule_id"]
def _set_led_info(self, info, params):
"""Set or remove values as per the device behaviour."""
info["get_led_info"]["led_status"] = params["led_rule"] != "never"
info["get_led_info"]["led_rule"] = params["led_rule"]
def _send_request(self, request_dict: dict):
method = request_dict["method"]
params = request_dict["params"]
@@ -218,7 +223,9 @@ class FakeSmartTransport(BaseTransport):
# SMART fixtures started to be generated
missing_result := self.FIXTURE_MISSING_MAP.get(method)
) and missing_result[0] in self.components:
result = copy.deepcopy(missing_result[1])
# Copy to info so it will work with update methods
info[method] = copy.deepcopy(missing_result[1])
result = copy.deepcopy(info[method])
retval = {"result": result, "error_code": 0}
else:
# PARAMS error returned for KS240 when get_device_usage called
@@ -234,11 +241,14 @@ class FakeSmartTransport(BaseTransport):
pytest.fixtures_missing_methods[self.fixture_name] = set()
pytest.fixtures_missing_methods[self.fixture_name].add(method)
return retval
elif method == "set_qs_info":
elif method in ["set_qs_info", "fw_download"]:
return {"error_code": 0}
elif method == "set_dynamic_light_effect_rule_enable":
self._set_light_effect(info, params)
return {"error_code": 0}
elif method == "set_led_info":
self._set_led_info(info, params)
return {"error_code": 0}
elif method[:4] == "set_":
target_method = f"get_{method[4:]}"
info[target_method].update(params)

View File

@@ -0,0 +1,547 @@
{
"component_nego": {
"component_list": [
{
"id": "device",
"ver_code": 2
},
{
"id": "firmware",
"ver_code": 2
},
{
"id": "quick_setup",
"ver_code": 3
},
{
"id": "inherit",
"ver_code": 1
},
{
"id": "time",
"ver_code": 1
},
{
"id": "wireless",
"ver_code": 1
},
{
"id": "account",
"ver_code": 1
},
{
"id": "synchronize",
"ver_code": 1
},
{
"id": "sunrise_sunset",
"ver_code": 1
},
{
"id": "led",
"ver_code": 1
},
{
"id": "cloud_connect",
"ver_code": 1
},
{
"id": "iot_cloud",
"ver_code": 1
},
{
"id": "child_device",
"ver_code": 1
},
{
"id": "child_quick_setup",
"ver_code": 1
},
{
"id": "child_inherit",
"ver_code": 1
},
{
"id": "control_child",
"ver_code": 1
},
{
"id": "alarm",
"ver_code": 1
},
{
"id": "device_load",
"ver_code": 1
},
{
"id": "device_local_time",
"ver_code": 1
},
{
"id": "alarm_logs",
"ver_code": 1
},
{
"id": "localSmart",
"ver_code": 1
},
{
"id": "matter",
"ver_code": 3
},
{
"id": "chime",
"ver_code": 1
}
]
},
"discovery_result": {
"device_id": "00000000000000000000000000000000",
"device_model": "H100(EU)",
"device_type": "SMART.TAPOHUB",
"factory_default": false,
"ip": "127.0.0.123",
"is_support_iot_cloud": true,
"mac": "3C-52-A1-00-00-00",
"mgt_encrypt_schm": {
"encrypt_type": "AES",
"http_port": 80,
"is_support_https": false,
"lv": 2
},
"obd_src": "tplink",
"owner": "00000000000000000000000000000000"
},
"get_alarm_configure": {
"duration": 10,
"type": "Alarm 1",
"volume": "high"
},
"get_auto_update_info": {
"enable": false,
"random_range": 120,
"time": 180
},
"get_child_device_component_list": {
"child_component_list": [
{
"component_list": [
{
"id": "device",
"ver_code": 2
},
{
"id": "quick_setup",
"ver_code": 3
},
{
"id": "time",
"ver_code": 1
},
{
"id": "device_local_time",
"ver_code": 1
},
{
"id": "schedule",
"ver_code": 2
},
{
"id": "countdown",
"ver_code": 2
},
{
"id": "account",
"ver_code": 1
},
{
"id": "synchronize",
"ver_code": 1
},
{
"id": "sunrise_sunset",
"ver_code": 1
},
{
"id": "cloud_connect",
"ver_code": 1
},
{
"id": "iot_cloud",
"ver_code": 1
},
{
"id": "frost_protection",
"ver_code": 1
},
{
"id": "child_protection",
"ver_code": 1
},
{
"id": "temperature",
"ver_code": 1
},
{
"id": "temp_control",
"ver_code": 1
},
{
"id": "remove_scale",
"ver_code": 1
},
{
"id": "progress_calibration",
"ver_code": 1
},
{
"id": "early_start",
"ver_code": 1
},
{
"id": "temp_record",
"ver_code": 1
},
{
"id": "screen_setting",
"ver_code": 1
},
{
"id": "night_mode",
"ver_code": 1
},
{
"id": "smart_control_schedule",
"ver_code": 1
},
{
"id": "firmware",
"ver_code": 1
},
{
"id": "battery_detect",
"ver_code": 1
},
{
"id": "temperature_correction",
"ver_code": 1
},
{
"id": "window_open_detect",
"ver_code": 2
},
{
"id": "shutdown_mode",
"ver_code": 1
}
],
"device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
},
{
"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
}
],
"device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
}
],
"start_index": 0,
"sum": 2
},
"get_child_device_list": {
"child_device_list": [
{
"at_low_battery": false,
"avatar": "",
"battery_percentage": 100,
"bind_count": 5,
"category": "subg.trv",
"child_protection": false,
"current_temp": 22.9,
"device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
"frost_protection_on": false,
"fw_ver": "2.4.0 Build 230804 Rel.193040",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"jamming_rssi": -113,
"jamming_signal_level": 1,
"location": "",
"mac": "A842A1000000",
"max_control_temp": 30,
"min_control_temp": 5,
"model": "KE100",
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"parent_device_id": "0000000000000000000000000000000000000000",
"region": "Europe/Berlin",
"rssi": -7,
"signal_level": 3,
"specs": "EU",
"status": "online",
"status_follow_edge": false,
"target_temp": 23.0,
"temp_offset": 0,
"temp_unit": "celsius",
"trv_states": [
"heating"
],
"type": "SMART.KASAENERGY"
},
{
"at_low_battery": false,
"avatar": "",
"battery_percentage": 100,
"bind_count": 1,
"category": "subg.trigger.temp-hmdt-sensor",
"current_humidity": 62,
"current_humidity_exception": 2,
"current_temp": 24.0,
"current_temp_exception": 0,
"device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
"fw_ver": "1.7.0 Build 230424 Rel.170332",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"jamming_rssi": -115,
"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": -38,
"signal_level": 3,
"specs": "EU",
"status": "online",
"status_follow_edge": false,
"temp_unit": "celsius",
"type": "SMART.TAPOSENSOR"
}
],
"start_index": 0,
"sum": 2
},
"get_connect_cloud_state": {
"status": 0
},
"get_device_info": {
"avatar": "hub",
"device_id": "0000000000000000000000000000000000000000",
"fw_id": "00000000000000000000000000000000",
"fw_ver": "1.5.10 Build 240207 Rel.175759",
"has_set_location_info": true,
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"in_alarm": false,
"in_alarm_source": "",
"ip": "127.0.0.123",
"lang": "de_DE",
"latitude": 0,
"longitude": 0,
"mac": "3C-52-A1-00-00-00",
"model": "H100",
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"overheated": false,
"region": "Europe/Berlin",
"rssi": -60,
"signal_level": 2,
"specs": "EU",
"ssid": "I01BU0tFRF9TU0lEIw==",
"time_diff": 60,
"type": "SMART.TAPOHUB"
},
"get_device_load_info": {
"cur_load_num": 4,
"load_level": "light",
"max_load_num": 64,
"total_memory": 4352,
"used_memory": 1451
},
"get_device_time": {
"region": "Europe/Berlin",
"time_diff": 60,
"timestamp": 1714669215
},
"get_device_usage": {},
"get_fw_download_state": {
"auto_upgrade": false,
"download_progress": 0,
"reboot_time": 5,
"status": 0,
"upgrade_time": 5
},
"get_inherit_info": null,
"get_latest_fw": {
"fw_size": 0,
"fw_ver": "1.5.10 Build 240207 Rel.175759",
"hw_id": "",
"need_to_upgrade": false,
"oem_id": "",
"release_date": "",
"release_note": "",
"type": 0
},
"get_led_info": {
"led_rule": "never",
"led_status": false,
"night_mode": {
"end_time": 358,
"night_mode_type": "sunrise_sunset",
"start_time": 1259,
"sunrise_offset": 0,
"sunset_offset": 0
}
},
"get_matter_setup_info": {
"setup_code": "00000000000",
"setup_payload": "00:0000000000000000000"
},
"get_support_alarm_type_list": {
"alarm_type_list": [
"Doorbell Ring 1",
"Doorbell Ring 2",
"Doorbell Ring 3",
"Doorbell Ring 4",
"Doorbell Ring 5",
"Doorbell Ring 6",
"Doorbell Ring 7",
"Doorbell Ring 8",
"Doorbell Ring 9",
"Doorbell Ring 10",
"Phone Ring",
"Alarm 1",
"Alarm 2",
"Alarm 3",
"Alarm 4",
"Dripping Tap",
"Alarm 5",
"Connection 1",
"Connection 2"
]
},
"get_support_child_device_category": {
"device_category_list": [
{
"category": "subg.trv"
},
{
"category": "subg.trigger"
},
{
"category": "subg.plugswitch"
}
]
},
"get_wireless_scan_info": {
"ap_list": [],
"start_index": 0,
"sum": 0,
"wep_supported": false
},
"qs_component_nego": {
"component_list": [
{
"id": "quick_setup",
"ver_code": 3
},
{
"id": "sunrise_sunset",
"ver_code": 1
},
{
"id": "iot_cloud",
"ver_code": 1
},
{
"id": "inherit",
"ver_code": 1
},
{
"id": "firmware",
"ver_code": 2
},
{
"id": "matter",
"ver_code": 3
}
],
"extra_info": {
"device_model": "H100",
"device_type": "SMART.TAPOHUB",
"is_klap": false
}
}
}

View File

@@ -0,0 +1,170 @@
{
"component_nego": {
"component_list": [
{
"id": "device",
"ver_code": 2
},
{
"id": "quick_setup",
"ver_code": 3
},
{
"id": "time",
"ver_code": 1
},
{
"id": "device_local_time",
"ver_code": 1
},
{
"id": "schedule",
"ver_code": 2
},
{
"id": "countdown",
"ver_code": 2
},
{
"id": "account",
"ver_code": 1
},
{
"id": "synchronize",
"ver_code": 1
},
{
"id": "sunrise_sunset",
"ver_code": 1
},
{
"id": "cloud_connect",
"ver_code": 1
},
{
"id": "iot_cloud",
"ver_code": 1
},
{
"id": "frost_protection",
"ver_code": 1
},
{
"id": "child_protection",
"ver_code": 1
},
{
"id": "temperature",
"ver_code": 1
},
{
"id": "temp_control",
"ver_code": 1
},
{
"id": "remove_scale",
"ver_code": 1
},
{
"id": "progress_calibration",
"ver_code": 1
},
{
"id": "early_start",
"ver_code": 1
},
{
"id": "temp_record",
"ver_code": 1
},
{
"id": "screen_setting",
"ver_code": 1
},
{
"id": "night_mode",
"ver_code": 1
},
{
"id": "smart_control_schedule",
"ver_code": 1
},
{
"id": "firmware",
"ver_code": 1
},
{
"id": "battery_detect",
"ver_code": 1
},
{
"id": "temperature_correction",
"ver_code": 1
},
{
"id": "window_open_detect",
"ver_code": 2
},
{
"id": "shutdown_mode",
"ver_code": 1
}
]
},
"get_connect_cloud_state": {
"status": 0
},
"get_device_info": {
"at_low_battery": false,
"avatar": "",
"battery_percentage": 100,
"bind_count": 5,
"category": "subg.trv",
"child_protection": false,
"current_temp": 22.9,
"device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
"frost_protection_on": false,
"fw_ver": "2.4.0 Build 230804 Rel.193040",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"jamming_rssi": -113,
"jamming_signal_level": 1,
"lastOnboardingTimestamp": 1713888871,
"location": "",
"mac": "A842A1000000",
"max_control_temp": 30,
"min_control_temp": 5,
"model": "KE100",
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"parent_device_id": "0000000000000000000000000000000000000000",
"region": "Europe/Berlin",
"rssi": -7,
"signal_level": 3,
"specs": "EU",
"status": "online",
"target_temp": 23.0,
"temp_offset": 0,
"temp_unit": "celsius",
"trv_states": [
"heating"
],
"type": "SMART.KASAENERGY"
},
"get_fw_download_state": {
"cloud_cache_seconds": 1,
"download_progress": 0,
"reboot_time": 5,
"status": 0,
"upgrade_time": 5
},
"get_latest_fw": {
"fw_ver": "2.8.0 Build 240202 Rel.135229",
"hw_id": "00000000000000000000000000000000",
"need_to_upgrade": true,
"oem_id": "00000000000000000000000000000000",
"release_date": "2024-02-05",
"release_note": "Modifications and Bug Fixes:\n1. Optimized the noise issue in some cases.\n2. Fixed some minor bugs.",
"type": 2
}
}

View File

@@ -0,0 +1,526 @@
{
"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
}
]
},
"get_connect_cloud_state": {
"status": 0
},
"get_device_info": {
"at_low_battery": false,
"avatar": "sensor_t110",
"bind_count": 1,
"category": "subg.trigger.contact-sensor",
"device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
"fw_ver": "1.8.0 Build 220728 Rel.160024",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"jamming_rssi": -113,
"jamming_signal_level": 1,
"lastOnboardingTimestamp": 1714661626,
"mac": "E4FAC4000000",
"model": "T110",
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"open": false,
"parent_device_id": "0000000000000000000000000000000000000000",
"region": "Europe/Berlin",
"report_interval": 16,
"rssi": -54,
"signal_level": 3,
"specs": "EU",
"status": "online",
"status_follow_edge": false,
"type": "SMART.TAPOSENSOR"
},
"get_fw_download_state": {
"cloud_cache_seconds": 1,
"download_progress": 30,
"reboot_time": 5,
"status": 4,
"upgrade_time": 5
},
"get_latest_fw": {
"fw_ver": "1.9.0 Build 230704 Rel.154531",
"hw_id": "00000000000000000000000000000000",
"need_to_upgrade": true,
"oem_id": "00000000000000000000000000000000",
"release_date": "2023-10-30",
"release_note": "Modifications and Bug Fixes:\n1. Reduced power consumption.\n2. Fixed some minor bugs.",
"type": 2
},
"get_temp_humidity_records": {
"local_time": 1714681046,
"past24h_humidity": [
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000
],
"past24h_humidity_exception": [
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000
],
"past24h_temp": [
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000
],
"past24h_temp_exception": [
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000,
-1000
],
"temp_unit": "celsius"
},
"get_trigger_logs": {
"logs": [
{
"event": "close",
"eventId": "8140289c-c66b-bdd6-63b9-542299442299",
"id": 4,
"timestamp": 1714661714
},
{
"event": "open",
"eventId": "fb4e1439-2f2c-a5e1-c35a-9e7c0d35a1e3",
"id": 3,
"timestamp": 1714661710
},
{
"event": "close",
"eventId": "ddee7733-1180-48ac-56a3-512018048ac5",
"id": 2,
"timestamp": 1714661657
},
{
"event": "open",
"eventId": "ab80951f-da38-49f9-21c5-bf025c7b606d",
"id": 1,
"timestamp": 1714661638
}
],
"start_id": 4,
"sum": 4
}
}

View File

@@ -2,7 +2,7 @@ import pytest
from kasa.iot import IotDevice
from kasa.smart import SmartDevice
from kasa.tests.conftest import dimmable, parametrize
from kasa.tests.conftest import dimmable_iot, parametrize
brightness = parametrize("brightness smart", component_filter="brightness")
@@ -10,13 +10,13 @@ brightness = parametrize("brightness smart", component_filter="brightness")
@brightness
async def test_brightness_component(dev: SmartDevice):
"""Test brightness feature."""
brightness = dev.get_module("Brightness")
brightness = dev.modules.get("Brightness")
assert brightness
assert isinstance(dev, SmartDevice)
assert "brightness" in dev._components
# Test getting the value
feature = brightness._module_features["brightness"]
feature = dev.features["brightness"]
assert isinstance(feature.value, int)
assert feature.value > 1 and feature.value <= 100
@@ -32,7 +32,7 @@ async def test_brightness_component(dev: SmartDevice):
await feature.set_value(feature.maximum_value + 10)
@dimmable
@dimmable_iot
async def test_brightness_dimmable(dev: IotDevice):
"""Test brightness feature."""
assert isinstance(dev, IotDevice)

View File

@@ -0,0 +1,28 @@
import pytest
from kasa import Module, SmartDevice
from kasa.tests.device_fixtures import parametrize
contact = parametrize(
"is contact sensor", model_filter="T110", protocol_filter={"SMART.CHILD"}
)
@contact
@pytest.mark.parametrize(
"feature, type",
[
("is_open", bool),
],
)
async def test_contact_features(dev: SmartDevice, feature, type):
"""Test that features are registered and work as expected."""
contact = dev.modules.get(Module.ContactSensor)
assert contact is not None
prop = getattr(contact, feature)
assert isinstance(prop, type)
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)

View File

@@ -1,8 +1,8 @@
import pytest
from pytest_mock import MockerFixture
from kasa import Module
from kasa.smart import SmartDevice
from kasa.smart.modules import FanModule
from kasa.tests.device_fixtures import parametrize
fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"SMART"})
@@ -11,10 +11,10 @@ fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"S
@fan
async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
"""Test fan speed feature."""
fan = dev.get_module(FanModule)
fan = dev.modules.get(Module.Fan)
assert fan
level_feature = fan._module_features["fan_speed_level"]
level_feature = dev.features["fan_speed_level"]
assert (
level_feature.minimum_value
<= level_feature.value
@@ -36,9 +36,9 @@ async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
@fan
async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
"""Test sleep mode feature."""
fan = dev.get_module(FanModule)
fan = dev.modules.get(Module.Fan)
assert fan
sleep_feature = fan._module_features["fan_sleep_mode"]
sleep_feature = dev.features["fan_sleep_mode"]
assert isinstance(sleep_feature.value, bool)
call = mocker.spy(fan, "call")
@@ -52,29 +52,28 @@ async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
@fan
async def test_fan_interface(dev: SmartDevice, mocker: MockerFixture):
async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
"""Test fan speed on device interface."""
assert isinstance(dev, SmartDevice)
fan = dev.get_module(FanModule)
fan = dev.modules.get(Module.Fan)
assert fan
device = fan._device
assert device.is_fan
await device.set_fan_speed_level(1)
await fan.set_fan_speed_level(1)
await dev.update()
assert device.fan_speed_level == 1
assert fan.fan_speed_level == 1
assert device.is_on
await device.set_fan_speed_level(4)
await fan.set_fan_speed_level(4)
await dev.update()
assert device.fan_speed_level == 4
assert fan.fan_speed_level == 4
await device.set_fan_speed_level(0)
await fan.set_fan_speed_level(0)
await dev.update()
assert not device.is_on
with pytest.raises(ValueError):
await device.set_fan_speed_level(-1)
await fan.set_fan_speed_level(-1)
with pytest.raises(ValueError):
await device.set_fan_speed_level(5)
await fan.set_fan_speed_level(5)

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
import asyncio
import logging
import pytest
from pytest_mock import MockerFixture
from kasa import Module
from kasa.smart import SmartDevice
from kasa.smart.modules.firmware import DownloadState, Firmware
from kasa.tests.device_fixtures import parametrize
firmware = parametrize(
"has firmware", component_filter="firmware", protocol_filter={"SMART"}
)
@firmware
@pytest.mark.parametrize(
"feature, prop_name, type, required_version",
[
("auto_update_enabled", "auto_update_enabled", bool, 2),
("update_available", "update_available", bool, 1),
("update_available", "update_available", bool, 1),
("current_firmware_version", "current_firmware", str, 1),
("available_firmware_version", "latest_firmware", str, 1),
],
)
async def test_firmware_features(
dev: SmartDevice, feature, prop_name, type, required_version, mocker: MockerFixture
):
"""Test light effect."""
fw = dev.modules.get(Module.Firmware)
assert fw
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
return
if not dev.is_cloud_connected:
pytest.skip("Device is not cloud connected, skipping test")
if fw.supported_version < required_version:
pytest.skip("Feature %s requires newer version" % feature)
prop = getattr(fw, prop_name)
assert isinstance(prop, type)
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)
@firmware
async def test_update_available_without_cloud(dev: SmartDevice):
"""Test that update_available returns None when disconnected."""
fw = dev.modules.get(Module.Firmware)
assert fw
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
return
if dev.is_cloud_connected:
assert isinstance(fw.update_available, bool)
else:
assert fw.update_available is None
@firmware
async def test_firmware_update(
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
):
"""Test updating firmware."""
caplog.set_level(logging.INFO)
fw = dev.modules.get(Module.Firmware)
assert fw
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
return
upgrade_time = 5
extras = {"reboot_time": 5, "upgrade_time": upgrade_time, "auto_upgrade": False}
update_states = [
# Unknown 1
DownloadState(status=1, download_progress=0, **extras),
# Downloading
DownloadState(status=2, download_progress=10, **extras),
DownloadState(status=2, download_progress=100, **extras),
# Flashing
DownloadState(status=3, download_progress=100, **extras),
DownloadState(status=3, download_progress=100, **extras),
# Done
DownloadState(status=0, download_progress=100, **extras),
]
asyncio_sleep = asyncio.sleep
sleep = mocker.patch("asyncio.sleep")
mocker.patch.object(fw, "get_update_state", side_effect=update_states)
cb_mock = mocker.AsyncMock()
await fw.update(progress_cb=cb_mock)
# This is necessary to allow the eventloop to process the created tasks
await asyncio_sleep(0)
assert "Unhandled state code" in caplog.text
assert "Downloading firmware, progress: 10" in caplog.text
assert "Flashing firmware, sleeping" in caplog.text
assert "Update idle" in caplog.text
for state in update_states:
cb_mock.assert_any_await(state)
# sleep based on the upgrade_time
sleep.assert_any_call(upgrade_time)

View File

@@ -23,6 +23,6 @@ async def test_humidity_features(dev, feature, type):
prop = getattr(humidity, feature)
assert isinstance(prop, type)
feat = humidity._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)

View File

@@ -1,13 +1,12 @@
from __future__ import annotations
from itertools import chain
from typing import cast
import pytest
from pytest_mock import MockerFixture
from kasa import Device, Feature
from kasa.smart.modules import LightEffectModule
from kasa import Device, Feature, Module
from kasa.smart.modules import LightEffect
from kasa.tests.device_fixtures import parametrize
light_effect = parametrize(
@@ -18,10 +17,10 @@ light_effect = parametrize(
@light_effect
async def test_light_effect(dev: Device, mocker: MockerFixture):
"""Test light effect."""
light_effect = cast(LightEffectModule, dev.modules.get("LightEffectModule"))
assert light_effect
light_effect = dev.modules.get(Module.LightEffect)
assert isinstance(light_effect, LightEffect)
feature = light_effect._module_features["light_effect"]
feature = dev.features["light_effect"]
assert feature.type == Feature.Type.Choice
call = mocker.spy(light_effect, "call")
@@ -29,7 +28,7 @@ async def test_light_effect(dev: Device, mocker: MockerFixture):
assert feature.choices
for effect in chain(reversed(feature.choices), feature.choices):
await light_effect.set_effect(effect)
enable = effect != LightEffectModule.LIGHT_EFFECTS_OFF
enable = effect != LightEffect.LIGHT_EFFECTS_OFF
params: dict[str, bool | str] = {"enable": enable}
if enable:
params["id"] = light_effect._scenes_names_to_id[effect]

View File

@@ -29,7 +29,7 @@ async def test_temperature_features(dev, feature, type):
prop = getattr(temp_module, feature)
assert isinstance(prop, type)
feat = temp_module._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)
@@ -42,6 +42,6 @@ async def test_temperature_warning(dev):
assert hasattr(temp_module, "temperature_warning")
assert isinstance(temp_module.temperature_warning, bool)
feat = temp_module._module_features["temperature_warning"]
feat = dev.features["temperature_warning"]
assert feat.value == temp_module.temperature_warning
assert isinstance(feat.value, bool)

View File

@@ -28,7 +28,7 @@ async def test_temperature_control_features(dev, feature, type):
prop = getattr(temp_module, feature)
assert isinstance(prop, type)
feat = temp_module._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)

View File

@@ -25,7 +25,7 @@ async def test_waterleak_properties(dev, feature, prop_name, type):
prop = getattr(waterleak, prop_name)
assert isinstance(prop, type)
feat = waterleak._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)

View File

@@ -7,19 +7,18 @@ from voluptuous import (
Schema,
)
from kasa import Bulb, BulbPreset, Device, DeviceType, KasaException
from kasa import Device, DeviceType, KasaException, LightPreset, Module
from kasa.iot import IotBulb, IotDimmer
from kasa.smart import SmartDevice
from .conftest import (
bulb,
bulb_iot,
color_bulb,
color_bulb_iot,
dimmable,
dimmable_iot,
handle_turn_on,
non_color_bulb,
non_dimmable,
non_dimmable_iot,
non_variable_temp,
turn_on,
variable_temp,
@@ -65,19 +64,20 @@ async def test_get_light_state(dev: IotBulb):
@color_bulb
@turn_on
async def test_hsv(dev: Device, turn_on):
assert isinstance(dev, Bulb)
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
assert dev.is_color
assert light.is_color
hue, saturation, brightness = dev.hsv
hue, saturation, brightness = light.hsv
assert 0 <= hue <= 360
assert 0 <= saturation <= 100
assert 0 <= brightness <= 100
await dev.set_hsv(hue=1, saturation=1, value=1)
await light.set_hsv(hue=1, saturation=1, value=1)
await dev.update()
hue, saturation, brightness = dev.hsv
hue, saturation, brightness = light.hsv
assert hue == 1
assert saturation == 1
assert brightness == 1
@@ -96,57 +96,64 @@ async def test_set_hsv_transition(dev: IotBulb, mocker):
@color_bulb
@turn_on
async def test_invalid_hsv(dev: Bulb, turn_on):
async def test_invalid_hsv(dev: Device, turn_on):
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
assert dev.is_color
assert light.is_color
for invalid_hue in [-1, 361, 0.5]:
with pytest.raises(ValueError):
await dev.set_hsv(invalid_hue, 0, 0) # type: ignore[arg-type]
await light.set_hsv(invalid_hue, 0, 0) # type: ignore[arg-type]
for invalid_saturation in [-1, 101, 0.5]:
with pytest.raises(ValueError):
await dev.set_hsv(0, invalid_saturation, 0) # type: ignore[arg-type]
await light.set_hsv(0, invalid_saturation, 0) # type: ignore[arg-type]
for invalid_brightness in [-1, 101, 0.5]:
with pytest.raises(ValueError):
await dev.set_hsv(0, 0, invalid_brightness) # type: ignore[arg-type]
await light.set_hsv(0, 0, invalid_brightness) # type: ignore[arg-type]
@color_bulb
@pytest.mark.skip("requires color feature")
async def test_color_state_information(dev: Device):
assert isinstance(dev, Bulb)
light = dev.modules.get(Module.Light)
assert light
assert "HSV" in dev.state_information
assert dev.state_information["HSV"] == dev.hsv
assert dev.state_information["HSV"] == light.hsv
@non_color_bulb
async def test_hsv_on_non_color(dev: Bulb):
assert not dev.is_color
async def test_hsv_on_non_color(dev: Device):
light = dev.modules.get(Module.Light)
assert light
assert not light.is_color
with pytest.raises(KasaException):
await dev.set_hsv(0, 0, 0)
await light.set_hsv(0, 0, 0)
with pytest.raises(KasaException):
print(dev.hsv)
print(light.hsv)
@variable_temp
@pytest.mark.skip("requires colortemp module")
async def test_variable_temp_state_information(dev: Device):
assert isinstance(dev, Bulb)
light = dev.modules.get(Module.Light)
assert light
assert "Color temperature" in dev.state_information
assert dev.state_information["Color temperature"] == dev.color_temp
assert dev.state_information["Color temperature"] == light.color_temp
@variable_temp
@turn_on
async def test_try_set_colortemp(dev: Device, turn_on):
assert isinstance(dev, Bulb)
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
await dev.set_color_temp(2700)
await light.set_color_temp(2700)
await dev.update()
assert dev.color_temp == 2700
assert light.color_temp == 2700
@variable_temp_iot
@@ -166,36 +173,42 @@ async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
@variable_temp_smart
async def test_smart_temp_range(dev: SmartDevice):
assert dev.valid_temperature_range
async def test_smart_temp_range(dev: Device):
light = dev.modules.get(Module.Light)
assert light
assert light.valid_temperature_range
@variable_temp
async def test_out_of_range_temperature(dev: Bulb):
async def test_out_of_range_temperature(dev: Device):
light = dev.modules.get(Module.Light)
assert light
with pytest.raises(ValueError):
await dev.set_color_temp(1000)
await light.set_color_temp(1000)
with pytest.raises(ValueError):
await dev.set_color_temp(10000)
await light.set_color_temp(10000)
@non_variable_temp
async def test_non_variable_temp(dev: Bulb):
async def test_non_variable_temp(dev: Device):
light = dev.modules.get(Module.Light)
assert light
with pytest.raises(KasaException):
await dev.set_color_temp(2700)
await light.set_color_temp(2700)
with pytest.raises(KasaException):
print(dev.valid_temperature_range)
print(light.valid_temperature_range)
with pytest.raises(KasaException):
print(dev.color_temp)
print(light.color_temp)
@dimmable
@dimmable_iot
@turn_on
async def test_dimmable_brightness(dev: Device, turn_on):
assert isinstance(dev, (Bulb, IotDimmer))
async def test_dimmable_brightness(dev: IotBulb, turn_on):
assert isinstance(dev, (IotBulb, IotDimmer))
await handle_turn_on(dev, turn_on)
assert dev.is_dimmable
assert dev._is_dimmable
await dev.set_brightness(50)
await dev.update()
@@ -229,9 +242,9 @@ async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
set_light_state.assert_called_with({"brightness": 10}, transition=1000)
@dimmable
async def test_invalid_brightness(dev: Bulb):
assert dev.is_dimmable
@dimmable_iot
async def test_invalid_brightness(dev: IotBulb):
assert dev._is_dimmable
with pytest.raises(ValueError):
await dev.set_brightness(110)
@@ -240,9 +253,9 @@ async def test_invalid_brightness(dev: Bulb):
await dev.set_brightness(-100)
@non_dimmable
async def test_non_dimmable(dev: Bulb):
assert not dev.is_dimmable
@non_dimmable_iot
async def test_non_dimmable(dev: IotBulb):
assert not dev._is_dimmable
with pytest.raises(KasaException):
assert dev.brightness == 0
@@ -291,7 +304,7 @@ async def test_modify_preset(dev: IotBulb, mocker):
"saturation": 0,
"color_temp": 0,
}
preset = BulbPreset(**data)
preset = LightPreset(**data)
assert preset.index == 0
assert preset.brightness == 10
@@ -305,7 +318,7 @@ async def test_modify_preset(dev: IotBulb, mocker):
with pytest.raises(KasaException):
await dev.save_preset(
BulbPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
LightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
)
@@ -314,11 +327,11 @@ async def test_modify_preset(dev: IotBulb, mocker):
("preset", "payload"),
[
(
BulbPreset(index=0, hue=0, brightness=1, saturation=0),
LightPreset(index=0, hue=0, brightness=1, saturation=0),
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
),
(
BulbPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
LightPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
),
],
@@ -380,7 +393,7 @@ SYSINFO_SCHEMA_BULB = SYSINFO_SCHEMA.extend(
@bulb
def test_device_type_bulb(dev):
def test_device_type_bulb(dev: Device):
if dev.is_light_strip:
pytest.skip("bulb has also lightstrips to test the api")
assert dev.device_type == DeviceType.Bulb

View File

@@ -13,6 +13,7 @@ from kasa import (
DeviceError,
EmeterStatus,
KasaException,
Module,
UnsupportedDeviceError,
)
from kasa.cli import (
@@ -21,11 +22,15 @@ from kasa.cli import (
brightness,
cli,
cmd_command,
effect,
emeter,
hsv,
led,
raw_command,
reboot,
state,
sysinfo,
temperature,
toggle,
update_credentials,
wifi,
@@ -34,7 +39,6 @@ from kasa.discover import Discover, DiscoveryResult
from kasa.iot import IotDevice
from .conftest import (
device_iot,
device_smart,
get_device_for_fixture_protocol,
handle_turn_on,
@@ -78,11 +82,10 @@ async def test_update_called_by_cli(dev, mocker, runner):
update.assert_called()
@device_iot
async def test_sysinfo(dev, runner):
async def test_sysinfo(dev: Device, runner):
res = await runner.invoke(sysinfo, obj=dev)
assert "System info" in res.output
assert dev.alias in res.output
assert dev.model in res.output
@turn_on
@@ -108,7 +111,6 @@ async def test_toggle(dev, turn_on, runner):
assert dev.is_on != turn_on
@device_iot
async def test_alias(dev, runner):
res = await runner.invoke(alias, obj=dev)
assert f"Alias: {dev.alias}" in res.output
@@ -308,15 +310,14 @@ async def test_emeter(dev: Device, mocker, runner):
daily.assert_called_with(year=1900, month=12)
@device_iot
async def test_brightness(dev, runner):
async def test_brightness(dev: Device, runner):
res = await runner.invoke(brightness, obj=dev)
if not dev.is_dimmable:
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
assert "This device does not support brightness." in res.output
return
res = await runner.invoke(brightness, obj=dev)
assert f"Brightness: {dev.brightness}" in res.output
assert f"Brightness: {light.brightness}" in res.output
res = await runner.invoke(brightness, ["12"], obj=dev)
assert "Setting brightness" in res.output
@@ -326,7 +327,110 @@ async def test_brightness(dev, runner):
assert "Brightness: 12" in res.output
@device_iot
async def test_color_temperature(dev: Device, runner):
res = await runner.invoke(temperature, obj=dev)
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
assert "Device does not support color temperature" in res.output
return
res = await runner.invoke(temperature, obj=dev)
assert f"Color temperature: {light.color_temp}" in res.output
valid_range = light.valid_temperature_range
assert f"(min: {valid_range.min}, max: {valid_range.max})" in res.output
val = int((valid_range.min + valid_range.max) / 2)
res = await runner.invoke(temperature, [str(val)], obj=dev)
assert "Setting color temperature to " in res.output
await dev.update()
res = await runner.invoke(temperature, obj=dev)
assert f"Color temperature: {val}" in res.output
assert res.exit_code == 0
invalid_max = valid_range.max + 100
# Lights that support the maximum range will not get past the click cli range check
# So can't be tested for the internal range check.
if invalid_max < 9000:
res = await runner.invoke(temperature, [str(invalid_max)], obj=dev)
assert res.exit_code == 1
assert isinstance(res.exception, ValueError)
res = await runner.invoke(temperature, [str(9100)], obj=dev)
assert res.exit_code == 2
async def test_color_hsv(dev: Device, runner: CliRunner):
res = await runner.invoke(hsv, obj=dev)
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
assert "Device does not support colors" in res.output
return
res = await runner.invoke(hsv, obj=dev)
assert f"Current HSV: {light.hsv}" in res.output
res = await runner.invoke(hsv, ["180", "50", "50"], obj=dev)
assert "Setting HSV: 180 50 50" in res.output
assert res.exit_code == 0
await dev.update()
res = await runner.invoke(hsv, ["180", "50"], obj=dev)
assert "Setting a color requires 3 values." in res.output
assert res.exit_code == 2
async def test_light_effect(dev: Device, runner: CliRunner):
res = await runner.invoke(effect, obj=dev)
if not (light_effect := dev.modules.get(Module.LightEffect)):
assert "Device does not support effects" in res.output
return
# Start off with a known state of off
await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF)
await dev.update()
assert light_effect.effect == light_effect.LIGHT_EFFECTS_OFF
res = await runner.invoke(effect, obj=dev)
msg = (
"Setting an effect requires a named built-in effect: "
+ f"{light_effect.effect_list}"
)
assert msg in res.output
assert res.exit_code == 2
res = await runner.invoke(effect, [light_effect.effect_list[1]], obj=dev)
assert f"Setting Effect: {light_effect.effect_list[1]}" in res.output
assert res.exit_code == 0
await dev.update()
assert light_effect.effect == light_effect.effect_list[1]
res = await runner.invoke(effect, ["foobar"], obj=dev)
assert f"Effect must be one of: {light_effect.effect_list}" in res.output
assert res.exit_code == 2
async def test_led(dev: Device, runner: CliRunner):
res = await runner.invoke(led, obj=dev)
if not (led_module := dev.modules.get(Module.Led)):
assert "Device does not support led" in res.output
return
res = await runner.invoke(led, obj=dev)
assert f"LED state: {led_module.led}" in res.output
assert res.exit_code == 0
res = await runner.invoke(led, ["on"], obj=dev)
assert "Turning led to True" in res.output
assert res.exit_code == 0
await dev.update()
assert led_module.led is True
res = await runner.invoke(led, ["off"], obj=dev)
assert "Turning led to False" in res.output
assert res.exit_code == 0
await dev.update()
assert led_module.led is False
async def test_json_output(dev: Device, mocker, runner):
"""Test that the json output produces correct output."""
mocker.patch("kasa.Discover.discover", return_value={"127.0.0.1": dev})
@@ -375,7 +479,6 @@ async def test_credentials(discovery_mock, mocker, runner):
assert "Username:foo Password:bar\n" in res.output
@device_iot
async def test_without_device_type(dev, mocker, runner):
"""Test connecting without the device type."""
discovery_mock = mocker.patch(
@@ -737,7 +840,7 @@ async def test_feature_set(mocker, runner):
dummy_device = await get_device_for_fixture_protocol(
"P300(EU)_1.0_1.0.13.json", "SMART"
)
led_setter = mocker.patch("kasa.smart.modules.ledmodule.LedModule.set_led")
led_setter = mocker.patch("kasa.smart.modules.led.Led.set_led")
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
res = await runner.invoke(

View File

@@ -0,0 +1,125 @@
import pytest
from pytest_mock import MockerFixture
from kasa import Device, Module
from kasa.tests.device_fixtures import (
dimmable_iot,
dimmer_iot,
lightstrip_iot,
parametrize,
parametrize_combine,
plug_iot,
)
led_smart = parametrize(
"has led smart", component_filter="led", protocol_filter={"SMART"}
)
led = parametrize_combine([led_smart, plug_iot])
light_effect_smart = parametrize(
"has light effect smart", component_filter="light_effect", protocol_filter={"SMART"}
)
light_effect = parametrize_combine([light_effect_smart, lightstrip_iot])
dimmable_smart = parametrize(
"dimmable smart", component_filter="brightness", protocol_filter={"SMART"}
)
dimmable = parametrize_combine([dimmable_smart, dimmer_iot, dimmable_iot])
@led
async def test_led_module(dev: Device, mocker: MockerFixture):
"""Test fan speed feature."""
led_module = dev.modules.get(Module.Led)
assert led_module
feat = dev.features["led"]
call = mocker.spy(led_module, "call")
await led_module.set_led(True)
assert call.call_count == 1
await dev.update()
assert led_module.led is True
assert feat.value is True
await led_module.set_led(False)
assert call.call_count == 2
await dev.update()
assert led_module.led is False
assert feat.value is False
await feat.set_value(True)
assert call.call_count == 3
await dev.update()
assert feat.value is True
assert led_module.led is True
@light_effect
async def test_light_effect_module(dev: Device, mocker: MockerFixture):
"""Test fan speed feature."""
light_effect_module = dev.modules[Module.LightEffect]
assert light_effect_module
feat = dev.features["light_effect"]
call = mocker.spy(light_effect_module, "call")
effect_list = light_effect_module.effect_list
assert "Off" in effect_list
assert effect_list.index("Off") == 0
assert len(effect_list) > 1
assert effect_list == feat.choices
assert light_effect_module.has_custom_effects is not None
await light_effect_module.set_effect("Off")
assert call.call_count == 1
await dev.update()
assert light_effect_module.effect == "Off"
assert feat.value == "Off"
second_effect = effect_list[1]
await light_effect_module.set_effect(second_effect)
assert call.call_count == 2
await dev.update()
assert light_effect_module.effect == second_effect
assert feat.value == second_effect
last_effect = effect_list[len(effect_list) - 1]
await light_effect_module.set_effect(last_effect)
assert call.call_count == 3
await dev.update()
assert light_effect_module.effect == last_effect
assert feat.value == last_effect
# Test feature set
await feat.set_value(second_effect)
assert call.call_count == 4
await dev.update()
assert light_effect_module.effect == second_effect
assert feat.value == second_effect
with pytest.raises(ValueError):
await light_effect_module.set_effect("foobar")
assert call.call_count == 4
@dimmable
async def test_light_brightness(dev: Device):
"""Test brightness setter and getter."""
assert isinstance(dev, Device)
light = dev.modules.get(Module.Light)
assert light
# Test getting the value
feature = dev.features["brightness"]
assert feature.minimum_value == 0
assert feature.maximum_value == 100
await light.set_brightness(10)
await dev.update()
assert light.brightness == 10
with pytest.raises(ValueError):
await light.set_brightness(feature.minimum_value - 10)
with pytest.raises(ValueError):
await light.set_brightness(feature.maximum_value + 10)

View File

@@ -9,7 +9,7 @@ from unittest.mock import Mock, patch
import pytest
import kasa
from kasa import Credentials, Device, DeviceConfig
from kasa import Credentials, Device, DeviceConfig, DeviceType
from kasa.iot import IotDevice
from kasa.smart import SmartChildDevice, SmartDevice
@@ -25,6 +25,7 @@ def _get_subclasses(of_class):
inspect.isclass(obj)
and issubclass(obj, of_class)
and module.__package__ != "kasa"
and module.__package__ != "kasa.interfaces"
):
subclasses.add((module.__package__ + "." + name, obj))
return subclasses
@@ -120,3 +121,56 @@ def test_deprecated_exceptions(exceptions_class, use_class):
with pytest.deprecated_call(match=msg):
getattr(kasa, exceptions_class)
getattr(kasa, use_class.__name__)
deprecated_is_device_type = {
"is_bulb": DeviceType.Bulb,
"is_plug": DeviceType.Plug,
"is_dimmer": DeviceType.Dimmer,
"is_light_strip": DeviceType.LightStrip,
"is_wallswitch": DeviceType.WallSwitch,
"is_strip": DeviceType.Strip,
"is_strip_socket": DeviceType.StripSocket,
}
deprecated_is_light_function_smart_module = {
"is_color": "Color",
"is_dimmable": "Brightness",
"is_variable_color_temp": "ColorTemperature",
}
def test_deprecated_attributes(dev: SmartDevice):
"""Test deprecated attributes on all devices."""
tested_keys = set()
def _test_attr(attribute):
tested_keys.add(attribute)
msg = f"{attribute} is deprecated"
if module := Device._deprecated_attributes[attribute][0]:
msg += f", use: {module} in device.modules instead"
with pytest.deprecated_call(match=msg):
val = getattr(dev, attribute)
return val
for attribute in deprecated_is_device_type:
val = _test_attr(attribute)
expected_val = dev.device_type == deprecated_is_device_type[attribute]
assert val == expected_val
for attribute in deprecated_is_light_function_smart_module:
val = _test_attr(attribute)
if isinstance(dev, SmartDevice):
expected_val = (
deprecated_is_light_function_smart_module[attribute] in dev.modules
)
elif hasattr(dev, f"_{attribute}"):
expected_val = getattr(dev, f"_{attribute}")
else:
expected_val = False
assert val == expected_val
assert len(tested_keys) == len(Device._deprecated_attributes)
untested_keys = [
key for key in Device._deprecated_attributes if key not in tested_keys
]
assert len(untested_keys) == 0

View File

@@ -3,10 +3,10 @@ import pytest
from kasa import DeviceType
from kasa.iot import IotDimmer
from .conftest import dimmer, handle_turn_on, turn_on
from .conftest import dimmer_iot, handle_turn_on, turn_on
@dimmer
@dimmer_iot
@turn_on
async def test_set_brightness(dev, turn_on):
await handle_turn_on(dev, turn_on)
@@ -22,7 +22,7 @@ async def test_set_brightness(dev, turn_on):
assert dev.is_on == turn_on
@dimmer
@dimmer_iot
@turn_on
async def test_set_brightness_transition(dev, turn_on, mocker):
await handle_turn_on(dev, turn_on)
@@ -44,7 +44,7 @@ async def test_set_brightness_transition(dev, turn_on, mocker):
assert dev.brightness == 1
@dimmer
@dimmer_iot
async def test_set_brightness_invalid(dev):
for invalid_brightness in [-1, 101, 0.5]:
with pytest.raises(ValueError):
@@ -55,7 +55,7 @@ async def test_set_brightness_invalid(dev):
await dev.set_brightness(1, transition=invalid_transition)
@dimmer
@dimmer_iot
async def test_turn_on_transition(dev, mocker):
query_helper = mocker.spy(IotDimmer, "_query_helper")
original_brightness = dev.brightness
@@ -72,7 +72,7 @@ async def test_turn_on_transition(dev, mocker):
assert dev.brightness == original_brightness
@dimmer
@dimmer_iot
async def test_turn_off_transition(dev, mocker):
await handle_turn_on(dev, True)
query_helper = mocker.spy(IotDimmer, "_query_helper")
@@ -90,7 +90,7 @@ async def test_turn_off_transition(dev, mocker):
)
@dimmer
@dimmer_iot
@turn_on
async def test_set_dimmer_transition(dev, turn_on, mocker):
await handle_turn_on(dev, turn_on)
@@ -108,7 +108,7 @@ async def test_set_dimmer_transition(dev, turn_on, mocker):
assert dev.brightness == 99
@dimmer
@dimmer_iot
@turn_on
async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
await handle_turn_on(dev, turn_on)
@@ -127,7 +127,7 @@ async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
)
@dimmer
@dimmer_iot
async def test_set_dimmer_transition_invalid(dev):
for invalid_brightness in [-1, 101, 0.5]:
with pytest.raises(ValueError):
@@ -138,6 +138,6 @@ async def test_set_dimmer_transition_invalid(dev):
await dev.set_dimmer_transition(1, invalid_transition)
@dimmer
@dimmer_iot
def test_device_type_dimmer(dev):
assert dev.device_type == DeviceType.Dimmer

View File

@@ -26,8 +26,8 @@ from kasa.xortransport import XorEncryption
from .conftest import (
bulb_iot,
dimmer,
lightstrip,
dimmer_iot,
lightstrip_iot,
new_discovery,
plug_iot,
strip_iot,
@@ -86,14 +86,14 @@ async def test_type_detection_strip(dev: Device):
assert d.device_type == DeviceType.Strip
@dimmer
@dimmer_iot
async def test_type_detection_dimmer(dev: Device):
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_dimmer
assert d.device_type == DeviceType.Dimmer
@lightstrip
@lightstrip_iot
async def test_type_detection_lightstrip(dev: Device):
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_light_strip

View File

@@ -1,5 +1,6 @@
import logging
import sys
from unittest.mock import patch
import pytest
from pytest_mock import MockerFixture
@@ -180,11 +181,10 @@ async def test_feature_setters(dev: Device, mocker: MockerFixture):
async def _test_features(dev):
exceptions = []
query = mocker.patch.object(dev.protocol, "query")
for feat in dev.features.values():
query.reset_mock()
try:
await _test_feature(feat, query)
with patch.object(feat.device.protocol, "query") as query:
await _test_feature(feat, query)
# we allow our own exceptions to avoid mocking valid responses
except KasaException:
pass

View File

@@ -16,7 +16,7 @@ from voluptuous import (
Schema,
)
from kasa import KasaException
from kasa import KasaException, Module
from kasa.iot import IotDevice
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
@@ -261,27 +261,26 @@ async def test_modules_not_supported(dev: IotDevice):
async def test_get_modules():
"""Test get_modules for child and parent modules."""
"""Test getting modules for child and parent modules."""
dummy_device = await get_device_for_fixture_protocol(
"HS100(US)_2.0_1.5.6.json", "IOT"
)
from kasa.iot.modules import Cloud
from kasa.smart.modules import CloudModule
# Modules on device
module = dummy_device.get_module("Cloud")
module = dummy_device.modules.get("cloud")
assert module
assert module._device == dummy_device
assert isinstance(module, Cloud)
module = dummy_device.get_module(Cloud)
module = dummy_device.modules.get(Module.IotCloud)
assert module
assert module._device == dummy_device
assert isinstance(module, Cloud)
# Invalid modules
module = dummy_device.get_module("DummyModule")
module = dummy_device.modules.get("DummyModule")
assert module is None
module = dummy_device.get_module(CloudModule)
module = dummy_device.modules.get(Module.Cloud)
assert module is None

View File

@@ -1,29 +1,28 @@
import pytest
from kasa import DeviceType
from kasa.exceptions import KasaException
from kasa.iot import IotLightStrip
from .conftest import lightstrip
from .conftest import lightstrip_iot
@lightstrip
@lightstrip_iot
async def test_lightstrip_length(dev: IotLightStrip):
assert dev.is_light_strip
assert dev.device_type == DeviceType.LightStrip
assert dev.length == dev.sys_info["length"]
@lightstrip
@lightstrip_iot
async def test_lightstrip_effect(dev: IotLightStrip):
assert isinstance(dev.effect, dict)
for k in ["brightness", "custom", "enable", "id", "name"]:
assert k in dev.effect
@lightstrip
@lightstrip_iot
async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
with pytest.raises(KasaException):
with pytest.raises(ValueError):
await dev.set_effect("Not real")
await dev.set_effect("Candy Cane")
@@ -31,7 +30,7 @@ async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
assert dev.effect["name"] == "Candy Cane"
@lightstrip
@lightstrip_iot
@pytest.mark.parametrize("brightness", [100, 50])
async def test_effects_lightstrip_set_effect_brightness(
dev: IotLightStrip, brightness, mocker
@@ -49,7 +48,7 @@ async def test_effects_lightstrip_set_effect_brightness(
assert payload["brightness"] == brightness
@lightstrip
@lightstrip_iot
@pytest.mark.parametrize("transition", [500, 1000])
async def test_effects_lightstrip_set_effect_transition(
dev: IotLightStrip, transition, mocker
@@ -67,12 +66,12 @@ async def test_effects_lightstrip_set_effect_transition(
assert payload["transition"] == transition
@lightstrip
@lightstrip_iot
async def test_effects_lightstrip_has_effects(dev: IotLightStrip):
assert dev.has_effects is True
assert dev.effect_list
@lightstrip
@lightstrip_iot
def test_device_type_lightstrip(dev):
assert dev.device_type == DeviceType.LightStrip

View File

@@ -9,12 +9,11 @@ from unittest.mock import patch
import pytest
from pytest_mock import MockerFixture
from kasa import KasaException
from kasa import KasaException, Module
from kasa.exceptions import SmartErrorCode
from kasa.smart import SmartDevice
from .conftest import (
bulb_smart,
device_smart,
get_device_for_fixture_protocol,
)
@@ -123,65 +122,42 @@ async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture):
async def test_get_modules():
"""Test get_modules for child and parent modules."""
"""Test getting modules for child and parent modules."""
dummy_device = await get_device_for_fixture_protocol(
"KS240(US)_1.0_1.0.5.json", "SMART"
)
from kasa.iot.modules import AmbientLight
from kasa.smart.modules import CloudModule, FanModule
from kasa.smart.modules import Cloud
# Modules on device
module = dummy_device.get_module("CloudModule")
module = dummy_device.modules.get("Cloud")
assert module
assert module._device == dummy_device
assert isinstance(module, CloudModule)
assert isinstance(module, Cloud)
module = dummy_device.get_module(CloudModule)
module = dummy_device.modules.get(Module.Cloud)
assert module
assert module._device == dummy_device
assert isinstance(module, CloudModule)
assert isinstance(module, Cloud)
# Modules on child
module = dummy_device.get_module("FanModule")
module = dummy_device.modules.get("Fan")
assert module
assert module._device != dummy_device
assert module._device._parent == dummy_device
module = dummy_device.get_module(FanModule)
module = dummy_device.modules.get(Module.Fan)
assert module
assert module._device != dummy_device
assert module._device._parent == dummy_device
# Invalid modules
module = dummy_device.get_module("DummyModule")
module = dummy_device.modules.get("DummyModule")
assert module is None
module = dummy_device.get_module(AmbientLight)
module = dummy_device.modules.get(Module.IotAmbientLight)
assert module is None
@bulb_smart
async def test_smartdevice_brightness(dev: SmartDevice):
"""Test brightness setter and getter."""
assert isinstance(dev, SmartDevice)
assert "brightness" in dev._components
# Test getting the value
feature = dev.features["brightness"]
assert feature.minimum_value == 1
assert feature.maximum_value == 100
await dev.set_brightness(10)
await dev.update()
assert dev.brightness == 10
with pytest.raises(ValueError):
await dev.set_brightness(feature.minimum_value - 10)
with pytest.raises(ValueError):
await dev.set_brightness(feature.maximum_value + 10)
@device_smart
async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixture):
"""Test is_cloud_connected property."""