Add support for KH100 hub (#847)

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 - **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 - **Bulbs**: KL110, KL120, KL125, KL130, KL135, KL50, KL60, LB110
- **Light Strips**: KL400L5, KL420L5, KL430 - **Light Strips**: KL400L5, KL420L5, KL430
- **Hubs**: KH100<sup>\*</sup>
### Supported Tapo<sup>\*</sup> devices ### 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.8
- Hardware: 2.0 (US) / Firmware: 1.0.9 - Hardware: 2.0 (US) / Firmware: 1.0.9
### Hubs
- **KH100**
- Hardware: 1.0 (UK) / Firmware: 1.5.6<sup>\*</sup>
## Tapo devices ## Tapo devices

View File

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

View File

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

View File

@ -28,6 +28,7 @@ class TemperatureSensor(SmartModule):
icon="mdi:thermometer", icon="mdi:thermometer",
) )
) )
if "current_temp_exception" in device.sys_info:
self._add_feature( self._add_feature(
Feature( Feature(
device, device,
@ -57,7 +58,7 @@ class TemperatureSensor(SmartModule):
@property @property
def temperature_warning(self) -> bool: def temperature_warning(self) -> bool:
"""Return True if temperature 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 return self._device.sys_info.get("current_temp_exception", 0) != 0
@property @property
def temperature_unit(self): def temperature_unit(self):

View File

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

View File

@ -107,8 +107,9 @@ DIMMERS = {
*DIMMERS_SMART, *DIMMERS_SMART,
} }
HUBS_SMART = {"H100"} HUBS_SMART = {"H100", "KH100"}
SENSORS_SMART = {"T315"} SENSORS_SMART = {"T310", "T315"}
THERMOSTATS_SMART = {"KE100"}
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT} WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
WITH_EMETER_SMART = {"P110", "KP125M", "EP25"} WITH_EMETER_SMART = {"P110", "KP125M", "EP25"}
@ -126,6 +127,7 @@ ALL_DEVICES_SMART = (
.union(HUBS_SMART) .union(HUBS_SMART)
.union(SENSORS_SMART) .union(SENSORS_SMART)
.union(SWITCHES_SMART) .union(SWITCHES_SMART)
.union(THERMOSTATS_SMART)
) )
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART) ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
@ -275,6 +277,9 @@ hubs_smart = parametrize(
sensors_smart = parametrize( sensors_smart = parametrize(
"sensors smart", model_filter=SENSORS_SMART, protocol_filter={"SMART.CHILD"} "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( device_smart = parametrize(
"devices smart", model_filter=ALL_DEVICES_SMART, protocol_filter={"SMART"} "devices smart", model_filter=ALL_DEVICES_SMART, protocol_filter={"SMART"}
) )
@ -296,6 +301,7 @@ def check_categories():
+ dimmers_smart.args[1] + dimmers_smart.args[1]
+ hubs_smart.args[1] + hubs_smart.args[1]
+ sensors_smart.args[1] + sensors_smart.args[1]
+ thermostats_smart.args[1]
) )
diffs: set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures) diffs: set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures)
if diffs: if diffs:
@ -313,7 +319,12 @@ check_categories()
def device_for_fixture_name(model, protocol): def device_for_fixture_name(model, protocol):
if "SMART" in protocol: if "SMART" in protocol:
for d in chain( 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: if d in model:
return SmartDevice 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"} "has temperature", component_filter="temperature", protocol_filter={"SMART.CHILD"}
) )
temperature_warning = parametrize(
"has temperature warning",
component_filter="comfort_temperature",
protocol_filter={"SMART.CHILD"},
)
@temperature @temperature
@pytest.mark.parametrize( @pytest.mark.parametrize(
"feature, type", "feature, type",
[ [
("temperature", float), ("temperature", float),
("temperature_warning", bool),
("temperature_unit", str), ("temperature_unit", str),
], ],
) )
@ -27,3 +32,16 @@ async def test_temperature_features(dev, feature, type):
feat = temp_module._module_features[feature] feat = temp_module._module_features[feature]
assert feat.value == prop assert feat.value == prop
assert isinstance(feat.value, type) 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 import pytest
from kasa.smart.modules import TemperatureSensor from kasa.smart.modules import TemperatureSensor
from kasa.tests.device_fixtures import parametrize from kasa.tests.device_fixtures import parametrize, thermostats_smart
temperature = parametrize( temperature = parametrize(
"has temperature control", "has temperature control",
@ -10,11 +10,11 @@ temperature = parametrize(
) )
@temperature @thermostats_smart
@pytest.mark.parametrize( @pytest.mark.parametrize(
"feature, type", "feature, type",
[ [
("target_temperature", int), ("target_temperature", float),
("temperature_offset", int), ("temperature_offset", int),
], ],
) )