Add support for KH100 hub ()

Add SMART.KASAHUB to the map of supported devices.
This also adds fixture files for KH100, KE100, and T310, and adapts affected modules and their tests accordingly.

---------

Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com>
This commit is contained in:
Adrian Dörr 2024-04-22 15:24:15 +01:00 committed by GitHub
parent e7d6758b8d
commit 0ab7436eef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 2488 additions and 21 deletions

View File

@ -231,6 +231,7 @@ The following devices have been tested and confirmed as working. If your device
- **Wall Switches**: ES20M, HS200, HS210, HS220, KP405, KS200M, KS205<sup>\*</sup>, KS220M, KS225<sup>\*</sup>, KS230
- **Bulbs**: KL110, KL120, KL125, KL130, KL135, KL50, KL60, LB110
- **Light Strips**: KL400L5, KL420L5, KL430
- **Hubs**: KH100<sup>\*</sup>
### Supported Tapo<sup>\*</sup> devices

View File

@ -126,6 +126,11 @@ Some newer Kasa devices require authentication. These are marked with <sup>*</su
- Hardware: 2.0 (US) / Firmware: 1.0.8
- Hardware: 2.0 (US) / Firmware: 1.0.9
### Hubs
- **KH100**
- Hardware: 1.0 (UK) / Firmware: 1.5.6<sup>\*</sup>
## Tapo devices

View File

@ -166,6 +166,7 @@ def get_device_class_from_family(device_type: str) -> type[Device] | None:
"SMART.TAPOSWITCH": SmartBulb,
"SMART.KASAPLUG": SmartDevice,
"SMART.TAPOHUB": SmartDevice,
"SMART.KASAHUB": SmartDevice,
"SMART.KASASWITCH": SmartBulb,
"IOT.SMARTPLUGSWITCH": IotPlug,
"IOT.SMARTBULB": IotBulb,

View File

@ -39,6 +39,7 @@ class DeviceFamilyType(Enum):
SmartTapoBulb = "SMART.TAPOBULB"
SmartTapoSwitch = "SMART.TAPOSWITCH"
SmartTapoHub = "SMART.TAPOHUB"
SmartKasaHub = "SMART.KASAHUB"
def _dataclass_from_dict(klass, in_val):

View File

@ -28,6 +28,7 @@ class TemperatureSensor(SmartModule):
icon="mdi:thermometer",
)
)
if "current_temp_exception" in device.sys_info:
self._add_feature(
Feature(
device,
@ -57,7 +58,7 @@ class TemperatureSensor(SmartModule):
@property
def temperature_warning(self) -> bool:
"""Return True if temperature is outside of the wanted range."""
return self._device.sys_info["current_temp_exception"] != 0
return self._device.sys_info.get("current_temp_exception", 0) != 0
@property
def temperature_unit(self):

View File

@ -14,7 +14,7 @@ if TYPE_CHECKING:
class TemperatureControl(SmartModule):
"""Implementation of temperature module."""
REQUIRED_COMPONENT = "temperature_control"
REQUIRED_COMPONENT = "temp_control"
def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module)
@ -57,11 +57,11 @@ class TemperatureControl(SmartModule):
return self._device.sys_info["max_control_temp"]
@property
def target_temperature(self) -> int:
def target_temperature(self) -> float:
"""Return target temperature."""
return self._device.sys_info["target_temperature"]
return self._device.sys_info["target_temp"]
async def set_target_temperature(self, target: int):
async def set_target_temperature(self, target: float):
"""Set target temperature."""
if (
target < self.minimum_target_temperature

View File

@ -107,8 +107,9 @@ DIMMERS = {
*DIMMERS_SMART,
}
HUBS_SMART = {"H100"}
SENSORS_SMART = {"T315"}
HUBS_SMART = {"H100", "KH100"}
SENSORS_SMART = {"T310", "T315"}
THERMOSTATS_SMART = {"KE100"}
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
WITH_EMETER_SMART = {"P110", "KP125M", "EP25"}
@ -126,6 +127,7 @@ ALL_DEVICES_SMART = (
.union(HUBS_SMART)
.union(SENSORS_SMART)
.union(SWITCHES_SMART)
.union(THERMOSTATS_SMART)
)
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
@ -275,6 +277,9 @@ hubs_smart = parametrize(
sensors_smart = parametrize(
"sensors smart", model_filter=SENSORS_SMART, protocol_filter={"SMART.CHILD"}
)
thermostats_smart = parametrize(
"thermostats smart", model_filter=THERMOSTATS_SMART, protocol_filter={"SMART.CHILD"}
)
device_smart = parametrize(
"devices smart", model_filter=ALL_DEVICES_SMART, protocol_filter={"SMART"}
)
@ -296,6 +301,7 @@ def check_categories():
+ dimmers_smart.args[1]
+ hubs_smart.args[1]
+ sensors_smart.args[1]
+ thermostats_smart.args[1]
)
diffs: set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures)
if diffs:
@ -313,7 +319,12 @@ check_categories()
def device_for_fixture_name(model, protocol):
if "SMART" in protocol:
for d in chain(
PLUGS_SMART, SWITCHES_SMART, STRIPS_SMART, HUBS_SMART, SENSORS_SMART
PLUGS_SMART,
SWITCHES_SMART,
STRIPS_SMART,
HUBS_SMART,
SENSORS_SMART,
THERMOSTATS_SMART,
):
if d in model:
return SmartDevice

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,171 @@
{
"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": "shutdown_mode",
"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
}
]
},
"get_connect_cloud_state": {
"status": 0
},
"get_device_info": {
"at_low_battery": false,
"avatar": "kasa_trv",
"battery_percentage": 100,
"bind_count": 1,
"category": "subg.trv",
"child_protection": false,
"current_temp": 19.2,
"device_id": "SCRUBBED_CHILD_DEVICE_ID_7",
"frost_protection_on": false,
"fw_ver": "2.8.0 Build 240202 Rel.135229",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"jamming_rssi": -121,
"jamming_signal_level": 1,
"lastOnboardingTimestamp": 1705684116,
"location": "",
"mac": "F0A731000000",
"max_control_temp": 30,
"min_control_temp": 5,
"model": "KE100",
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"parent_device_id": "0000000000000000000000000000000000000000",
"region": "Europe/London",
"rssi": -73,
"signal_level": 2,
"specs": "EU",
"status": "online",
"target_temp": 21.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_size": 0,
"fw_ver": "2.8.0 Build 240202 Rel.135229",
"hw_id": "",
"need_to_upgrade": false,
"oem_id": "",
"release_date": "",
"release_note": "",
"type": 0
}
}

View File

@ -0,0 +1,171 @@
{
"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": "shutdown_mode",
"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
}
]
},
"get_connect_cloud_state": {
"status": 0
},
"get_device_info": {
"at_low_battery": false,
"avatar": "kasa_trv",
"battery_percentage": 100,
"bind_count": 1,
"category": "subg.trv",
"child_protection": false,
"current_temp": 20.1,
"device_id": "SCRUBBED_CHILD_DEVICE_ID_3",
"frost_protection_on": false,
"fw_ver": "2.8.0 Build 240202 Rel.135229",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"jamming_rssi": -117,
"jamming_signal_level": 1,
"lastOnboardingTimestamp": 1705677078,
"location": "",
"mac": "F0A731000000",
"max_control_temp": 30,
"min_control_temp": 5,
"model": "KE100",
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"parent_device_id": "0000000000000000000000000000000000000000",
"region": "Europe/London",
"rssi": -45,
"signal_level": 3,
"specs": "UK",
"status": "online",
"target_temp": 21.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_size": 0,
"fw_ver": "2.8.0 Build 240202 Rel.135229",
"hw_id": "",
"need_to_upgrade": false,
"oem_id": "",
"release_date": "",
"release_note": "",
"type": 0
}
}

View File

@ -0,0 +1,530 @@
{
"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": "sensor_t310",
"bind_count": 1,
"category": "subg.trigger.temp-hmdt-sensor",
"current_humidity": 54,
"current_humidity_exception": 0,
"current_temp": 19.3,
"current_temp_exception": 0,
"device_id": "SCRUBBED_CHILD_DEVICE_ID_10",
"fw_ver": "1.5.0 Build 230105 Rel.150707",
"hw_id": "00000000000000000000000000000000",
"hw_ver": "1.0",
"jamming_rssi": -120,
"jamming_signal_level": 1,
"lastOnboardingTimestamp": 1706789728,
"mac": "E4FAC4000000",
"model": "T310",
"nickname": "I01BU0tFRF9OQU1FIw==",
"oem_id": "00000000000000000000000000000000",
"parent_device_id": "0000000000000000000000000000000000000000",
"region": "Europe/London",
"report_interval": 16,
"rssi": -81,
"signal_level": 1,
"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_size": 0,
"fw_ver": "1.5.0 Build 230105 Rel.150707",
"hw_id": "",
"need_to_upgrade": false,
"oem_id": "",
"release_date": "",
"release_note": "",
"type": 0
},
"get_temp_humidity_records": {
"local_time": 1713550233,
"past24h_humidity": [
60,
60,
60,
60,
60,
60,
60,
60,
60,
61,
61,
61,
61,
61,
61,
61,
61,
61,
61,
61,
61,
62,
61,
61,
62,
61,
60,
61,
61,
61,
61,
61,
61,
61,
61,
61,
61,
62,
62,
62,
62,
62,
62,
62,
62,
62,
62,
62,
62,
62,
62,
63,
63,
63,
64,
63,
63,
63,
63,
62,
63,
63,
62,
62,
62,
62,
62,
61,
62,
61,
61,
61,
61,
61,
61,
60,
61,
64,
64,
61,
61,
63,
60,
60,
60,
60,
59,
59,
59,
59,
59,
58,
58,
58,
57,
55
],
"past24h_humidity_exception": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
2,
1,
1,
2,
1,
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
3,
3,
3,
4,
3,
3,
3,
3,
2,
3,
3,
2,
2,
2,
2,
2,
1,
2,
1,
1,
1,
1,
1,
1,
0,
1,
4,
4,
1,
1,
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"past24h_temp": [
175,
175,
174,
174,
173,
172,
172,
171,
170,
169,
169,
167,
167,
166,
165,
164,
163,
163,
162,
162,
162,
162,
163,
163,
162,
162,
161,
160,
159,
159,
159,
159,
158,
158,
159,
159,
158,
159,
159,
159,
159,
159,
159,
159,
159,
159,
158,
158,
158,
158,
158,
158,
159,
159,
160,
161,
161,
162,
162,
162,
162,
162,
163,
163,
166,
168,
170,
172,
174,
175,
176,
177,
179,
181,
183,
184,
185,
187,
189,
190,
190,
193,
194,
194,
194,
194,
194,
194,
195,
195,
195,
196,
196,
196,
195,
193
],
"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,
-1,
-1,
-1,
-1,
-2,
-2,
-1,
-1,
-2,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
-2,
-2,
-2,
-2,
-2,
-2,
-1,
-1,
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
],
"temp_unit": "celsius"
},
"get_trigger_logs": {
"logs": [],
"start_id": 0,
"sum": 0
}
}

View File

@ -7,13 +7,18 @@ temperature = parametrize(
"has temperature", component_filter="temperature", protocol_filter={"SMART.CHILD"}
)
temperature_warning = parametrize(
"has temperature warning",
component_filter="comfort_temperature",
protocol_filter={"SMART.CHILD"},
)
@temperature
@pytest.mark.parametrize(
"feature, type",
[
("temperature", float),
("temperature_warning", bool),
("temperature_unit", str),
],
)
@ -27,3 +32,16 @@ async def test_temperature_features(dev, feature, type):
feat = temp_module._module_features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)
@temperature_warning
async def test_temperature_warning(dev):
"""Test that features are registered and work as expected."""
temp_module: TemperatureSensor = dev.modules["TemperatureSensor"]
assert hasattr(temp_module, "temperature_warning")
assert isinstance(temp_module.temperature_warning, bool)
feat = temp_module._module_features["temperature_warning"]
assert feat.value == temp_module.temperature_warning
assert isinstance(feat.value, bool)

View File

@ -1,7 +1,7 @@
import pytest
from kasa.smart.modules import TemperatureSensor
from kasa.tests.device_fixtures import parametrize
from kasa.tests.device_fixtures import parametrize, thermostats_smart
temperature = parametrize(
"has temperature control",
@ -10,11 +10,11 @@ temperature = parametrize(
)
@temperature
@thermostats_smart
@pytest.mark.parametrize(
"feature, type",
[
("target_temperature", int),
("target_temperature", float),
("temperature_offset", int),
],
)