mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-01-22 20:57:07 +00:00
Initial support for vacuums (clean module) (#944)
Adds support for clean module: - Show current vacuum state - Start cleaning (all rooms) - Return to dock - Pausing & unpausing - Controlling the fan speed --------- Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com>
This commit is contained in:
parent
be34dbd387
commit
1be87674bf
@ -204,6 +204,7 @@ The following devices have been tested and confirmed as working. If your device
|
|||||||
- **Cameras**: C100, C210, C225, C325WB, C520WS, C720, D230, TC65, TC70
|
- **Cameras**: C100, C210, C225, C325WB, C520WS, C720, D230, TC65, TC70
|
||||||
- **Hubs**: H100, H200
|
- **Hubs**: H100, H200
|
||||||
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
|
- **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315
|
||||||
|
- **Vacuums**: RV20 Max Plus
|
||||||
|
|
||||||
<!--SUPPORTED_END-->
|
<!--SUPPORTED_END-->
|
||||||
[^1]: Model requires authentication
|
[^1]: Model requires authentication
|
||||||
|
@ -324,6 +324,11 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
|
|||||||
- Hardware: 1.0 (EU) / Firmware: 1.7.0
|
- Hardware: 1.0 (EU) / Firmware: 1.7.0
|
||||||
- Hardware: 1.0 (US) / Firmware: 1.8.0
|
- Hardware: 1.0 (US) / Firmware: 1.8.0
|
||||||
|
|
||||||
|
### Vacuums
|
||||||
|
|
||||||
|
- **RV20 Max Plus**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.0.7
|
||||||
|
|
||||||
|
|
||||||
<!--SUPPORTED_END-->
|
<!--SUPPORTED_END-->
|
||||||
[^1]: Model requires authentication
|
[^1]: Model requires authentication
|
||||||
|
@ -39,6 +39,7 @@ DEVICE_TYPE_TO_PRODUCT_GROUP = {
|
|||||||
DeviceType.Hub: "Hubs",
|
DeviceType.Hub: "Hubs",
|
||||||
DeviceType.Sensor: "Hub-Connected Devices",
|
DeviceType.Sensor: "Hub-Connected Devices",
|
||||||
DeviceType.Thermostat: "Hub-Connected Devices",
|
DeviceType.Thermostat: "Hub-Connected Devices",
|
||||||
|
DeviceType.Vacuum: "Vacuums",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,6 +118,16 @@ class SmartRequest:
|
|||||||
enable: bool
|
enable: bool
|
||||||
id: str | None = None
|
id: str | None = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GetCleanAttrParams(SmartRequestParams):
|
||||||
|
"""CleanAttr params.
|
||||||
|
|
||||||
|
Decides which cleaning settings are requested
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: type can be global or pose
|
||||||
|
type: str = "global"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_raw_request(
|
def get_raw_request(
|
||||||
method: str, params: SmartRequestParams | None = None
|
method: str, params: SmartRequestParams | None = None
|
||||||
@ -429,6 +439,8 @@ COMPONENT_REQUESTS = {
|
|||||||
"clean": [
|
"clean": [
|
||||||
SmartRequest.get_raw_request("getCleanRecords"),
|
SmartRequest.get_raw_request("getCleanRecords"),
|
||||||
SmartRequest.get_raw_request("getVacStatus"),
|
SmartRequest.get_raw_request("getVacStatus"),
|
||||||
|
SmartRequest.get_raw_request("getCleanStatus"),
|
||||||
|
SmartRequest("getCleanAttr", SmartRequest.GetCleanAttrParams()),
|
||||||
],
|
],
|
||||||
"battery": [SmartRequest.get_raw_request("getBatteryInfo")],
|
"battery": [SmartRequest.get_raw_request("getBatteryInfo")],
|
||||||
"consumables": [SmartRequest.get_raw_request("getConsumablesInfo")],
|
"consumables": [SmartRequest.get_raw_request("getConsumablesInfo")],
|
||||||
|
@ -159,7 +159,7 @@ def get_device_class_from_family(
|
|||||||
"SMART.KASAHUB": SmartDevice,
|
"SMART.KASAHUB": SmartDevice,
|
||||||
"SMART.KASASWITCH": SmartDevice,
|
"SMART.KASASWITCH": SmartDevice,
|
||||||
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
|
"SMART.IPCAMERA.HTTPS": SmartCamDevice,
|
||||||
"SMART.TAPOROBOVAC": SmartDevice,
|
"SMART.TAPOROBOVAC.HTTPS": SmartDevice,
|
||||||
"IOT.SMARTPLUGSWITCH": IotPlug,
|
"IOT.SMARTPLUGSWITCH": IotPlug,
|
||||||
"IOT.SMARTBULB": IotBulb,
|
"IOT.SMARTBULB": IotBulb,
|
||||||
"IOT.IPCAMERA": IotCamera,
|
"IOT.IPCAMERA": IotCamera,
|
||||||
@ -173,6 +173,9 @@ def get_device_class_from_family(
|
|||||||
_LOGGER.debug("Unknown SMART device with %s, using SmartDevice", device_type)
|
_LOGGER.debug("Unknown SMART device with %s, using SmartDevice", device_type)
|
||||||
cls = SmartDevice
|
cls = SmartDevice
|
||||||
|
|
||||||
|
if cls is not None:
|
||||||
|
_LOGGER.debug("Using %s for %s", cls.__name__, device_type)
|
||||||
|
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
@ -188,6 +191,7 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol
|
|||||||
"""
|
"""
|
||||||
ctype = config.connection_type
|
ctype = config.connection_type
|
||||||
protocol_name = ctype.device_family.value.split(".")[0]
|
protocol_name = ctype.device_family.value.split(".")[0]
|
||||||
|
_LOGGER.debug("Finding protocol for %s", ctype.device_family)
|
||||||
|
|
||||||
if ctype.device_family is DeviceFamily.SmartIpCamera:
|
if ctype.device_family is DeviceFamily.SmartIpCamera:
|
||||||
if strict and ctype.encryption_type is not DeviceEncryptionType.Aes:
|
if strict and ctype.encryption_type is not DeviceEncryptionType.Aes:
|
||||||
|
@ -676,9 +676,14 @@ class Discover:
|
|||||||
for key, val in candidates.items():
|
for key, val in candidates.items():
|
||||||
try:
|
try:
|
||||||
prot, config = val
|
prot, config = val
|
||||||
|
_LOGGER.debug("Trying to connect with %s", prot.__class__.__name__)
|
||||||
dev = await _connect(config, prot)
|
dev = await _connect(config, prot)
|
||||||
except Exception:
|
except Exception as ex:
|
||||||
_LOGGER.debug("Unable to connect with %s", prot)
|
_LOGGER.debug(
|
||||||
|
"Unable to connect with %s: %s",
|
||||||
|
prot.__class__.__name__,
|
||||||
|
ex,
|
||||||
|
)
|
||||||
if on_attempt:
|
if on_attempt:
|
||||||
ca = tuple.__new__(ConnectAttempt, key)
|
ca = tuple.__new__(ConnectAttempt, key)
|
||||||
on_attempt(ca, False)
|
on_attempt(ca, False)
|
||||||
@ -686,6 +691,7 @@ class Discover:
|
|||||||
if on_attempt:
|
if on_attempt:
|
||||||
ca = tuple.__new__(ConnectAttempt, key)
|
ca = tuple.__new__(ConnectAttempt, key)
|
||||||
on_attempt(ca, True)
|
on_attempt(ca, True)
|
||||||
|
_LOGGER.debug("Found working protocol %s", prot.__class__.__name__)
|
||||||
return dev
|
return dev
|
||||||
finally:
|
finally:
|
||||||
await prot.close()
|
await prot.close()
|
||||||
|
@ -127,6 +127,8 @@ class SmartErrorCode(IntEnum):
|
|||||||
DST_ERROR = -2301
|
DST_ERROR = -2301
|
||||||
DST_SAVE_ERROR = -2302
|
DST_SAVE_ERROR = -2302
|
||||||
|
|
||||||
|
VACUUM_BATTERY_LOW = -3001
|
||||||
|
|
||||||
SYSTEM_ERROR = -40101
|
SYSTEM_ERROR = -40101
|
||||||
INVALID_ARGUMENTS = -40209
|
INVALID_ARGUMENTS = -40209
|
||||||
|
|
||||||
|
@ -161,6 +161,9 @@ class Module(ABC):
|
|||||||
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
|
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
|
||||||
LensMask: Final[ModuleName[smartcam.LensMask]] = ModuleName("LensMask")
|
LensMask: Final[ModuleName[smartcam.LensMask]] = ModuleName("LensMask")
|
||||||
|
|
||||||
|
# Vacuum modules
|
||||||
|
Clean: Final[ModuleName[smart.Clean]] = ModuleName("Clean")
|
||||||
|
|
||||||
def __init__(self, device: Device, module: str) -> None:
|
def __init__(self, device: Device, module: str) -> None:
|
||||||
self._device = device
|
self._device = device
|
||||||
self._module = module
|
self._module = module
|
||||||
|
@ -7,6 +7,7 @@ from .batterysensor import BatterySensor
|
|||||||
from .brightness import Brightness
|
from .brightness import Brightness
|
||||||
from .childdevice import ChildDevice
|
from .childdevice import ChildDevice
|
||||||
from .childprotection import ChildProtection
|
from .childprotection import ChildProtection
|
||||||
|
from .clean import Clean
|
||||||
from .cloud import Cloud
|
from .cloud import Cloud
|
||||||
from .color import Color
|
from .color import Color
|
||||||
from .colortemperature import ColorTemperature
|
from .colortemperature import ColorTemperature
|
||||||
@ -66,6 +67,7 @@ __all__ = [
|
|||||||
"TriggerLogs",
|
"TriggerLogs",
|
||||||
"FrostProtection",
|
"FrostProtection",
|
||||||
"Thermostat",
|
"Thermostat",
|
||||||
|
"Clean",
|
||||||
"SmartLightEffect",
|
"SmartLightEffect",
|
||||||
"OverheatProtection",
|
"OverheatProtection",
|
||||||
"HomeKit",
|
"HomeKit",
|
||||||
|
267
kasa/smart/modules/clean.py
Normal file
267
kasa/smart/modules/clean.py
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
"""Implementation of vacuum clean module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from enum import IntEnum
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ...module import FeatureAttribute
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Status(IntEnum):
|
||||||
|
"""Status of vacuum."""
|
||||||
|
|
||||||
|
Idle = 0
|
||||||
|
Cleaning = 1
|
||||||
|
Mapping = 2
|
||||||
|
GoingHome = 4
|
||||||
|
Charging = 5
|
||||||
|
Charged = 6
|
||||||
|
Paused = 7
|
||||||
|
Undocked = 8
|
||||||
|
Error = 100
|
||||||
|
|
||||||
|
UnknownInternal = -1000
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorCode(IntEnum):
|
||||||
|
"""Error codes for vacuum."""
|
||||||
|
|
||||||
|
Ok = 0
|
||||||
|
SideBrushStuck = 2
|
||||||
|
MainBrushStuck = 3
|
||||||
|
WheelBlocked = 4
|
||||||
|
DustBinRemoved = 14
|
||||||
|
UnableToMove = 15
|
||||||
|
LidarBlocked = 16
|
||||||
|
UnableToFindDock = 21
|
||||||
|
BatteryLow = 22
|
||||||
|
|
||||||
|
UnknownInternal = -1000
|
||||||
|
|
||||||
|
|
||||||
|
class FanSpeed(IntEnum):
|
||||||
|
"""Fan speed level."""
|
||||||
|
|
||||||
|
Quiet = 1
|
||||||
|
Standard = 2
|
||||||
|
Turbo = 3
|
||||||
|
Max = 4
|
||||||
|
|
||||||
|
|
||||||
|
class Clean(SmartModule):
|
||||||
|
"""Implementation of vacuum clean module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = "clean"
|
||||||
|
_error_code = ErrorCode.Ok
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="vacuum_return_home",
|
||||||
|
name="Return home",
|
||||||
|
container=self,
|
||||||
|
attribute_setter="return_home",
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Action,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="vacuum_start",
|
||||||
|
name="Start cleaning",
|
||||||
|
container=self,
|
||||||
|
attribute_setter="start",
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Action,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="vacuum_pause",
|
||||||
|
name="Pause",
|
||||||
|
container=self,
|
||||||
|
attribute_setter="pause",
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Action,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="vacuum_status",
|
||||||
|
name="Vacuum status",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="status",
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="vacuum_error",
|
||||||
|
name="Error",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="error",
|
||||||
|
category=Feature.Category.Info,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="battery_level",
|
||||||
|
name="Battery level",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="battery",
|
||||||
|
icon="mdi:battery",
|
||||||
|
unit_getter=lambda: "%",
|
||||||
|
category=Feature.Category.Info,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="vacuum_fan_speed",
|
||||||
|
name="Fan speed",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="fan_speed_preset",
|
||||||
|
attribute_setter="set_fan_speed_preset",
|
||||||
|
icon="mdi:fan",
|
||||||
|
choices_getter=lambda: list(FanSpeed.__members__),
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Type.Choice,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _post_update_hook(self) -> None:
|
||||||
|
"""Set error code after update."""
|
||||||
|
errors = self._vac_status.get("err_status")
|
||||||
|
if errors is None or not errors:
|
||||||
|
self._error_code = ErrorCode.Ok
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(errors) > 1:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Multiple error codes, using the first one only: %s", errors
|
||||||
|
)
|
||||||
|
|
||||||
|
error = errors.pop(0)
|
||||||
|
try:
|
||||||
|
self._error_code = ErrorCode(error)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unknown error code, please create an issue describing the error: %s",
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
self._error_code = ErrorCode.UnknownInternal
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
return {
|
||||||
|
"getVacStatus": None,
|
||||||
|
"getBatteryInfo": None,
|
||||||
|
"getCleanStatus": None,
|
||||||
|
"getCleanAttr": {"type": "global"},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def start(self) -> dict:
|
||||||
|
"""Start cleaning."""
|
||||||
|
# If we are paused, do not restart cleaning
|
||||||
|
|
||||||
|
if self.status is Status.Paused:
|
||||||
|
return await self.resume()
|
||||||
|
|
||||||
|
return await self.call(
|
||||||
|
"setSwitchClean",
|
||||||
|
{
|
||||||
|
"clean_mode": 0,
|
||||||
|
"clean_on": True,
|
||||||
|
"clean_order": True,
|
||||||
|
"force_clean": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def pause(self) -> dict:
|
||||||
|
"""Pause cleaning."""
|
||||||
|
if self.status is Status.GoingHome:
|
||||||
|
return await self.set_return_home(False)
|
||||||
|
|
||||||
|
return await self.set_pause(True)
|
||||||
|
|
||||||
|
async def resume(self) -> dict:
|
||||||
|
"""Resume cleaning."""
|
||||||
|
return await self.set_pause(False)
|
||||||
|
|
||||||
|
async def set_pause(self, enabled: bool) -> dict:
|
||||||
|
"""Pause or resume cleaning."""
|
||||||
|
return await self.call("setRobotPause", {"pause": enabled})
|
||||||
|
|
||||||
|
async def return_home(self) -> dict:
|
||||||
|
"""Return home."""
|
||||||
|
return await self.set_return_home(True)
|
||||||
|
|
||||||
|
async def set_return_home(self, enabled: bool) -> dict:
|
||||||
|
"""Return home / pause returning."""
|
||||||
|
return await self.call("setSwitchCharge", {"switch_charge": enabled})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error(self) -> ErrorCode:
|
||||||
|
"""Return error."""
|
||||||
|
return self._error_code
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_speed_preset(self) -> Annotated[str, FeatureAttribute()]:
|
||||||
|
"""Return fan speed preset."""
|
||||||
|
return FanSpeed(self._settings["suction"]).name
|
||||||
|
|
||||||
|
async def set_fan_speed_preset(
|
||||||
|
self, speed: str
|
||||||
|
) -> Annotated[dict, FeatureAttribute]:
|
||||||
|
"""Set fan speed preset."""
|
||||||
|
name_to_value = {x.name: x.value for x in FanSpeed}
|
||||||
|
if speed not in name_to_value:
|
||||||
|
raise ValueError("Invalid fan speed %s, available %s", speed, name_to_value)
|
||||||
|
return await self.call(
|
||||||
|
"setCleanAttr", {"suction": name_to_value[speed], "type": "global"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery(self) -> int:
|
||||||
|
"""Return battery level."""
|
||||||
|
return self.data["getBatteryInfo"]["battery_percentage"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _vac_status(self) -> dict:
|
||||||
|
"""Return vac status container."""
|
||||||
|
return self.data["getVacStatus"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _settings(self) -> dict:
|
||||||
|
"""Return cleaning settings."""
|
||||||
|
return self.data["getCleanAttr"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> Status:
|
||||||
|
"""Return current status."""
|
||||||
|
if self._error_code is not ErrorCode.Ok:
|
||||||
|
return Status.Error
|
||||||
|
|
||||||
|
status_code = self._vac_status["status"]
|
||||||
|
try:
|
||||||
|
return Status(status_code)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning("Got unknown status code: %s (%s)", status_code, self.data)
|
||||||
|
return Status.UnknownInternal
|
@ -134,6 +134,8 @@ SENSORS_SMART = {
|
|||||||
}
|
}
|
||||||
THERMOSTATS_SMART = {"KE100"}
|
THERMOSTATS_SMART = {"KE100"}
|
||||||
|
|
||||||
|
VACUUMS_SMART = {"RV20"}
|
||||||
|
|
||||||
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
|
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
|
||||||
WITH_EMETER_SMART = {"P110", "P110M", "P115", "KP125M", "EP25", "P304M"}
|
WITH_EMETER_SMART = {"P110", "P110M", "P115", "KP125M", "EP25", "P304M"}
|
||||||
WITH_EMETER = {*WITH_EMETER_IOT, *WITH_EMETER_SMART}
|
WITH_EMETER = {*WITH_EMETER_IOT, *WITH_EMETER_SMART}
|
||||||
@ -151,6 +153,7 @@ ALL_DEVICES_SMART = (
|
|||||||
.union(SENSORS_SMART)
|
.union(SENSORS_SMART)
|
||||||
.union(SWITCHES_SMART)
|
.union(SWITCHES_SMART)
|
||||||
.union(THERMOSTATS_SMART)
|
.union(THERMOSTATS_SMART)
|
||||||
|
.union(VACUUMS_SMART)
|
||||||
)
|
)
|
||||||
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
||||||
|
|
||||||
@ -342,6 +345,7 @@ hub_smartcam = parametrize(
|
|||||||
device_type_filter=[DeviceType.Hub],
|
device_type_filter=[DeviceType.Hub],
|
||||||
protocol_filter={"SMARTCAM"},
|
protocol_filter={"SMARTCAM"},
|
||||||
)
|
)
|
||||||
|
vacuum = parametrize("vacuums", device_type_filter=[DeviceType.Vacuum])
|
||||||
|
|
||||||
|
|
||||||
def check_categories():
|
def check_categories():
|
||||||
@ -360,6 +364,7 @@ def check_categories():
|
|||||||
+ thermostats_smart.args[1]
|
+ thermostats_smart.args[1]
|
||||||
+ camera_smartcam.args[1]
|
+ camera_smartcam.args[1]
|
||||||
+ hub_smartcam.args[1]
|
+ hub_smartcam.args[1]
|
||||||
|
+ vacuum.args[1]
|
||||||
)
|
)
|
||||||
diffs: set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures)
|
diffs: set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures)
|
||||||
if diffs:
|
if diffs:
|
||||||
|
@ -383,8 +383,8 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
result = copy.deepcopy(info[child_method])
|
result = copy.deepcopy(info[child_method])
|
||||||
retval = {"result": result, "error_code": 0}
|
retval = {"result": result, "error_code": 0}
|
||||||
return retval
|
return retval
|
||||||
elif child_method[:4] == "set_":
|
elif child_method[:3] == "set":
|
||||||
target_method = f"get_{child_method[4:]}"
|
target_method = f"get{child_method[3:]}"
|
||||||
if target_method not in child_device_calls:
|
if target_method not in child_device_calls:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"No {target_method} in child info, calling set before get not supported."
|
f"No {target_method} in child info, calling set before get not supported."
|
||||||
@ -549,7 +549,7 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
return await self._handle_control_child(request_dict["params"])
|
return await self._handle_control_child(request_dict["params"])
|
||||||
|
|
||||||
params = request_dict.get("params", {})
|
params = request_dict.get("params", {})
|
||||||
if method in {"component_nego", "qs_component_nego"} or method[:4] == "get_":
|
if method in {"component_nego", "qs_component_nego"} or method[:3] == "get":
|
||||||
if method in info:
|
if method in info:
|
||||||
result = copy.deepcopy(info[method])
|
result = copy.deepcopy(info[method])
|
||||||
if result and "start_index" in result and "sum" in result:
|
if result and "start_index" in result and "sum" in result:
|
||||||
@ -637,9 +637,14 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
return self._set_on_off_gradually_info(info, params)
|
return self._set_on_off_gradually_info(info, params)
|
||||||
elif method == "set_child_protection":
|
elif method == "set_child_protection":
|
||||||
return self._update_sysinfo_key(info, "child_protection", params["enable"])
|
return self._update_sysinfo_key(info, "child_protection", params["enable"])
|
||||||
elif method[:4] == "set_":
|
elif method[:3] == "set":
|
||||||
target_method = f"get_{method[4:]}"
|
target_method = f"get{method[3:]}"
|
||||||
|
# Some vacuum commands do not have a getter
|
||||||
|
if method in ["setRobotPause", "setSwitchClean", "setSwitchCharge"]:
|
||||||
|
return {"error_code": 0}
|
||||||
|
|
||||||
info[target_method].update(params)
|
info[target_method].update(params)
|
||||||
|
|
||||||
return {"error_code": 0}
|
return {"error_code": 0}
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
|
310
tests/fixtures/smart/RV20 Max Plus(EU)_1.0_1.0.7.json
vendored
Normal file
310
tests/fixtures/smart/RV20 Max Plus(EU)_1.0_1.0.7.json
vendored
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
{
|
||||||
|
"component_nego": {
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "device",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clean",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "battery",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "consumables",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "direction_control",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "button_and_led",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "speaker",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "schedule",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wireless",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "auto_change_map",
|
||||||
|
"ver_code": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ble_whole_setup",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dust_bucket",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inherit",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mop",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "do_not_disturb",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_local_time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "charge_pose_clean",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "continue_breakpoint_sweep",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "goto_point",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "furniture",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_cloud_backup",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dev_log",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "map_lock",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "carpet_area",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clean_angle",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clean_percent",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "no_pose_config",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"discovery_result": {
|
||||||
|
"error_code": 0,
|
||||||
|
"result": {
|
||||||
|
"device_id": "00000000000000000000000000000000",
|
||||||
|
"device_model": "RV20 Max Plus(EU)",
|
||||||
|
"device_type": "SMART.TAPOROBOVAC",
|
||||||
|
"factory_default": false,
|
||||||
|
"ip": "127.0.0.123",
|
||||||
|
"is_support_iot_cloud": true,
|
||||||
|
"mac": "B0-19-21-00-00-00",
|
||||||
|
"mgt_encrypt_schm": {
|
||||||
|
"encrypt_type": "AES",
|
||||||
|
"http_port": 4433,
|
||||||
|
"is_support_https": true
|
||||||
|
},
|
||||||
|
"obd_src": "tplink",
|
||||||
|
"owner": "00000000000000000000000000000000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"getAutoChangeMap": {
|
||||||
|
"auto_change_map": false
|
||||||
|
},
|
||||||
|
"getAutoDustCollection": {
|
||||||
|
"auto_dust_collection": 1
|
||||||
|
},
|
||||||
|
"getBatteryInfo": {
|
||||||
|
"battery_percentage": 75
|
||||||
|
},
|
||||||
|
"getCleanAttr": {"suction": 2, "cistern": 2, "clean_number": 1},
|
||||||
|
"getCleanStatus": {"getCleanStatus": {"clean_status": 0, "is_working": false, "is_mapping": false, "is_relocating": false}},
|
||||||
|
"getCleanRecords": {
|
||||||
|
"lastest_day_record": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"record_list": [],
|
||||||
|
"record_list_num": 0,
|
||||||
|
"total_area": 0,
|
||||||
|
"total_number": 0,
|
||||||
|
"total_time": 0
|
||||||
|
},
|
||||||
|
"getConsumablesInfo": {
|
||||||
|
"charge_contact_time": 0,
|
||||||
|
"edge_brush_time": 0,
|
||||||
|
"filter_time": 0,
|
||||||
|
"main_brush_lid_time": 0,
|
||||||
|
"rag_time": 0,
|
||||||
|
"roll_brush_time": 0,
|
||||||
|
"sensor_time": 0
|
||||||
|
},
|
||||||
|
"getCurrentVoiceLanguage": {
|
||||||
|
"name": "2",
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"getDoNotDisturb": {
|
||||||
|
"do_not_disturb": true,
|
||||||
|
"e_min": 480,
|
||||||
|
"s_min": 1320
|
||||||
|
},
|
||||||
|
"getMapInfo": {
|
||||||
|
"auto_change_map": false,
|
||||||
|
"current_map_id": 0,
|
||||||
|
"map_list": [],
|
||||||
|
"map_num": 0,
|
||||||
|
"version": "LDS"
|
||||||
|
},
|
||||||
|
"getMopState": {
|
||||||
|
"mop_state": false
|
||||||
|
},
|
||||||
|
"getVacStatus": {
|
||||||
|
"err_status": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"errorCode_id": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"prompt": [],
|
||||||
|
"promptCode_id": [],
|
||||||
|
"status": 5
|
||||||
|
},
|
||||||
|
"get_device_info": {
|
||||||
|
"auto_pack_ver": "0.0.1.1771",
|
||||||
|
"avatar": "",
|
||||||
|
"board_sn": "000000000000",
|
||||||
|
"custom_sn": "000000000000",
|
||||||
|
"device_id": "0000000000000000000000000000000000000000",
|
||||||
|
"fw_id": "00000000000000000000000000000000",
|
||||||
|
"fw_ver": "1.0.7 Build 240828 Rel.205951",
|
||||||
|
"has_set_location_info": true,
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"hw_ver": "1.0",
|
||||||
|
"ip": "127.0.0.123",
|
||||||
|
"lang": "",
|
||||||
|
"latitude": 0,
|
||||||
|
"linux_ver": "V21.198.1708420747",
|
||||||
|
"location": "",
|
||||||
|
"longitude": 0,
|
||||||
|
"mac": "B0-19-21-00-00-00",
|
||||||
|
"mcu_ver": "1.1.2563.5",
|
||||||
|
"model": "RV20 Max Plus",
|
||||||
|
"nickname": "I01BU0tFRF9OQU1FIw==",
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"overheated": false,
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"rssi": -59,
|
||||||
|
"signal_level": 2,
|
||||||
|
"specs": "",
|
||||||
|
"ssid": "I01BU0tFRF9TU0lEIw==",
|
||||||
|
"sub_ver": "0.0.1.1771-1.1.34",
|
||||||
|
"time_diff": 60,
|
||||||
|
"total_ver": "1.1.34",
|
||||||
|
"type": "SMART.TAPOROBOVAC"
|
||||||
|
},
|
||||||
|
"get_device_time": {
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"time_diff": 60,
|
||||||
|
"timestamp": 1736598518
|
||||||
|
},
|
||||||
|
"get_fw_download_state": {
|
||||||
|
"auto_upgrade": false,
|
||||||
|
"download_progress": 0,
|
||||||
|
"reboot_time": 5,
|
||||||
|
"status": 0,
|
||||||
|
"upgrade_time": 5
|
||||||
|
},
|
||||||
|
"get_inherit_info": null,
|
||||||
|
"get_next_event": {},
|
||||||
|
"get_schedule_rules": {
|
||||||
|
"enable": false,
|
||||||
|
"rule_list": [],
|
||||||
|
"schedule_rule_max_count": 32,
|
||||||
|
"start_index": 0,
|
||||||
|
"sum": 0
|
||||||
|
},
|
||||||
|
"get_wireless_scan_info": {
|
||||||
|
"ap_list": [
|
||||||
|
{
|
||||||
|
"key_type": "wpa2_psk",
|
||||||
|
"signal_level": 2,
|
||||||
|
"ssid": "I01BU0tFRF9TU0lEIw=="
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_index": 0,
|
||||||
|
"sum": 1,
|
||||||
|
"wep_supported": true
|
||||||
|
},
|
||||||
|
"qs_component_nego": {
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ble_whole_setup",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inherit",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extra_info": {
|
||||||
|
"device_model": "RV20 Max Plus",
|
||||||
|
"device_type": "SMART.TAPOROBOVAC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
146
tests/smart/modules/test_clean.py
Normal file
146
tests/smart/modules/test_clean.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from kasa import Module
|
||||||
|
from kasa.smart import SmartDevice
|
||||||
|
from kasa.smart.modules.clean import ErrorCode, Status
|
||||||
|
|
||||||
|
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||||
|
|
||||||
|
clean = parametrize("clean module", component_filter="clean", protocol_filter={"SMART"})
|
||||||
|
|
||||||
|
|
||||||
|
@clean
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("feature", "prop_name", "type"),
|
||||||
|
[
|
||||||
|
("vacuum_status", "status", Status),
|
||||||
|
("vacuum_error", "error", ErrorCode),
|
||||||
|
("vacuum_fan_speed", "fan_speed_preset", str),
|
||||||
|
("battery_level", "battery", int),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
|
||||||
|
"""Test that features are registered and work as expected."""
|
||||||
|
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||||
|
assert clean is not None
|
||||||
|
|
||||||
|
prop = getattr(clean, prop_name)
|
||||||
|
assert isinstance(prop, type)
|
||||||
|
|
||||||
|
feat = clean._device.features[feature]
|
||||||
|
assert feat.value == prop
|
||||||
|
assert isinstance(feat.value, type)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("feature", "value", "method", "params"),
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"vacuum_start",
|
||||||
|
1,
|
||||||
|
"setSwitchClean",
|
||||||
|
{
|
||||||
|
"clean_mode": 0,
|
||||||
|
"clean_on": True,
|
||||||
|
"clean_order": True,
|
||||||
|
"force_clean": False,
|
||||||
|
},
|
||||||
|
id="vacuum_start",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"vacuum_pause", 1, "setRobotPause", {"pause": True}, id="vacuum_pause"
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"vacuum_return_home",
|
||||||
|
1,
|
||||||
|
"setSwitchCharge",
|
||||||
|
{"switch_charge": True},
|
||||||
|
id="vacuum_return_home",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"vacuum_fan_speed",
|
||||||
|
"Quiet",
|
||||||
|
"setCleanAttr",
|
||||||
|
{"suction": 1, "type": "global"},
|
||||||
|
id="vacuum_fan_speed",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@clean
|
||||||
|
async def test_actions(
|
||||||
|
dev: SmartDevice,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
feature: str,
|
||||||
|
value: str | int,
|
||||||
|
method: str,
|
||||||
|
params: dict,
|
||||||
|
):
|
||||||
|
"""Test the clean actions."""
|
||||||
|
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||||
|
call = mocker.spy(clean, "call")
|
||||||
|
|
||||||
|
await dev.features[feature].set_value(value)
|
||||||
|
call.assert_called_with(method, params)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("err_status", "error"),
|
||||||
|
[
|
||||||
|
pytest.param([], ErrorCode.Ok, id="empty error"),
|
||||||
|
pytest.param([0], ErrorCode.Ok, id="no error"),
|
||||||
|
pytest.param([3], ErrorCode.MainBrushStuck, id="known error"),
|
||||||
|
pytest.param([123], ErrorCode.UnknownInternal, id="unknown error"),
|
||||||
|
pytest.param([3, 4], ErrorCode.MainBrushStuck, id="multi-error"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@clean
|
||||||
|
async def test_post_update_hook(dev: SmartDevice, err_status: list, error: ErrorCode):
|
||||||
|
"""Test that post update hook sets error states correctly."""
|
||||||
|
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||||
|
clean.data["getVacStatus"]["err_status"] = err_status
|
||||||
|
|
||||||
|
await clean._post_update_hook()
|
||||||
|
|
||||||
|
assert clean._error_code is error
|
||||||
|
|
||||||
|
if error is not ErrorCode.Ok:
|
||||||
|
assert clean.status is Status.Error
|
||||||
|
|
||||||
|
|
||||||
|
@clean
|
||||||
|
async def test_resume(dev: SmartDevice, mocker: MockerFixture):
|
||||||
|
"""Test that start calls resume if the state is paused."""
|
||||||
|
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||||
|
|
||||||
|
call = mocker.spy(clean, "call")
|
||||||
|
resume = mocker.spy(clean, "resume")
|
||||||
|
|
||||||
|
mocker.patch.object(
|
||||||
|
type(clean),
|
||||||
|
"status",
|
||||||
|
new_callable=mocker.PropertyMock,
|
||||||
|
return_value=Status.Paused,
|
||||||
|
)
|
||||||
|
await clean.start()
|
||||||
|
|
||||||
|
call.assert_called_with("setRobotPause", {"pause": False})
|
||||||
|
resume.assert_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@clean
|
||||||
|
async def test_unknown_status(
|
||||||
|
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
|
||||||
|
):
|
||||||
|
"""Test that unknown status is logged."""
|
||||||
|
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||||
|
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
clean.data["getVacStatus"]["status"] = 123
|
||||||
|
|
||||||
|
assert clean.status is Status.UnknownInternal
|
||||||
|
assert "Got unknown status code: 123" in caplog.text
|
@ -117,7 +117,11 @@ async def test_connect_custom_port(discovery_mock, mocker, custom_port):
|
|||||||
connection_type=ctype,
|
connection_type=ctype,
|
||||||
credentials=Credentials("dummy_user", "dummy_password"),
|
credentials=Credentials("dummy_user", "dummy_password"),
|
||||||
)
|
)
|
||||||
default_port = 80 if "result" in discovery_data else 9999
|
default_port = (
|
||||||
|
DiscoveryResult.from_dict(discovery_data["result"]).mgt_encrypt_schm.http_port
|
||||||
|
if "result" in discovery_data
|
||||||
|
else 9999
|
||||||
|
)
|
||||||
|
|
||||||
ctype, _ = _get_connection_type_device_class(discovery_data)
|
ctype, _ = _get_connection_type_device_class(discovery_data)
|
||||||
|
|
||||||
|
@ -134,7 +134,14 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
|
|||||||
discovery_mock.ip = host
|
discovery_mock.ip = host
|
||||||
discovery_mock.port_override = custom_port
|
discovery_mock.port_override = custom_port
|
||||||
|
|
||||||
device_class = Discover._get_device_class(discovery_mock.discovery_data)
|
disco_data = discovery_mock.discovery_data
|
||||||
|
device_class = Discover._get_device_class(disco_data)
|
||||||
|
http_port = (
|
||||||
|
DiscoveryResult.from_dict(disco_data["result"]).mgt_encrypt_schm.http_port
|
||||||
|
if "result" in disco_data
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
# discovery_mock patches protocol query methods so use spy here.
|
# discovery_mock patches protocol query methods so use spy here.
|
||||||
update_mock = mocker.spy(device_class, "update")
|
update_mock = mocker.spy(device_class, "update")
|
||||||
|
|
||||||
@ -143,7 +150,11 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
|
|||||||
)
|
)
|
||||||
assert issubclass(x.__class__, Device)
|
assert issubclass(x.__class__, Device)
|
||||||
assert x._discovery_info is not None
|
assert x._discovery_info is not None
|
||||||
assert x.port == custom_port or x.port == discovery_mock.default_port
|
assert (
|
||||||
|
x.port == custom_port
|
||||||
|
or x.port == discovery_mock.default_port
|
||||||
|
or x.port == http_port
|
||||||
|
)
|
||||||
# Make sure discovery does not call update()
|
# Make sure discovery does not call update()
|
||||||
assert update_mock.call_count == 0
|
assert update_mock.call_count == 0
|
||||||
if discovery_mock.default_port == 80:
|
if discovery_mock.default_port == 80:
|
||||||
@ -153,6 +164,7 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
|
|||||||
discovery_mock.device_type,
|
discovery_mock.device_type,
|
||||||
discovery_mock.encrypt_type,
|
discovery_mock.encrypt_type,
|
||||||
discovery_mock.login_version,
|
discovery_mock.login_version,
|
||||||
|
discovery_mock.https,
|
||||||
)
|
)
|
||||||
config = DeviceConfig(
|
config = DeviceConfig(
|
||||||
host=host,
|
host=host,
|
||||||
@ -681,7 +693,7 @@ async def test_discover_try_connect_all(discovery_mock, mocker):
|
|||||||
and self._transport.__class__ is transport_class
|
and self._transport.__class__ is transport_class
|
||||||
):
|
):
|
||||||
return discovery_mock.query_data
|
return discovery_mock.query_data
|
||||||
raise KasaException()
|
raise KasaException("Unable to execute query")
|
||||||
|
|
||||||
async def _update(self, *args, **kwargs):
|
async def _update(self, *args, **kwargs):
|
||||||
if (
|
if (
|
||||||
@ -689,7 +701,8 @@ async def test_discover_try_connect_all(discovery_mock, mocker):
|
|||||||
and self.protocol._transport.__class__ is transport_class
|
and self.protocol._transport.__class__ is transport_class
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
raise KasaException()
|
|
||||||
|
raise KasaException("Unable to execute update")
|
||||||
|
|
||||||
mocker.patch("kasa.IotProtocol.query", new=_query)
|
mocker.patch("kasa.IotProtocol.query", new=_query)
|
||||||
mocker.patch("kasa.SmartProtocol.query", new=_query)
|
mocker.patch("kasa.SmartProtocol.query", new=_query)
|
||||||
|
Loading…
Reference in New Issue
Block a user