|
@@ -1,13 +1,18 @@
|
|
import datetime
|
|
import datetime
|
|
-from unittest.mock import AsyncMock
|
|
|
|
|
|
+from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
|
|
import pytest
|
|
import pytest
|
|
from bleak.backends.device import BLEDevice
|
|
from bleak.backends.device import BLEDevice
|
|
|
|
|
|
-from switchbot import SwitchBotAdvertisement, SwitchbotModel
|
|
|
|
-from switchbot.adv_parsers.humidifier import process_evaporative_humidifier
|
|
|
|
-from switchbot.const.evaporative_humidifier import HumidifierMode, HumidifierWaterLevel
|
|
|
|
|
|
+from switchbot import (
|
|
|
|
+ HumidifierAction,
|
|
|
|
+ HumidifierMode,
|
|
|
|
+ HumidifierWaterLevel,
|
|
|
|
+ SwitchBotAdvertisement,
|
|
|
|
+ SwitchbotModel,
|
|
|
|
+)
|
|
from switchbot.devices import evaporative_humidifier
|
|
from switchbot.devices import evaporative_humidifier
|
|
|
|
+from switchbot.devices.device import SwitchbotEncryptedDevice, SwitchbotOperationError
|
|
|
|
|
|
from .test_adv_parser import generate_ble_device
|
|
from .test_adv_parser import generate_ble_device
|
|
|
|
|
|
@@ -22,6 +27,9 @@ def create_device_for_command_testing(init_data: dict | None = None):
|
|
evaporative_humidifier_device.update_from_advertisement(
|
|
evaporative_humidifier_device.update_from_advertisement(
|
|
make_advertisement_data(ble_device, init_data)
|
|
make_advertisement_data(ble_device, init_data)
|
|
)
|
|
)
|
|
|
|
+ evaporative_humidifier_device._send_command = AsyncMock()
|
|
|
|
+ evaporative_humidifier_device._check_command_result = MagicMock()
|
|
|
|
+ evaporative_humidifier_device.update = AsyncMock()
|
|
return evaporative_humidifier_device
|
|
return evaporative_humidifier_device
|
|
|
|
|
|
|
|
|
|
@@ -36,17 +44,19 @@ def make_advertisement_data(ble_device: BLEDevice, init_data: dict | None = None
|
|
"data": {
|
|
"data": {
|
|
"isOn": False,
|
|
"isOn": False,
|
|
"mode": None,
|
|
"mode": None,
|
|
- "target_humidity": None,
|
|
|
|
|
|
+ "target_humidity": 52,
|
|
"child_lock": False,
|
|
"child_lock": False,
|
|
"over_humidify_protection": True,
|
|
"over_humidify_protection": True,
|
|
"tank_removed": False,
|
|
"tank_removed": False,
|
|
"tilted_alert": False,
|
|
"tilted_alert": False,
|
|
"filter_missing": False,
|
|
"filter_missing": False,
|
|
|
|
+ "is_meter_binded": True,
|
|
"humidity": 51,
|
|
"humidity": 51,
|
|
"temperature": 16.8,
|
|
"temperature": 16.8,
|
|
|
|
+ "temp": {"c": 16.8, "f": 62.24},
|
|
"filter_run_time": datetime.timedelta(days=3, seconds=57600),
|
|
"filter_run_time": datetime.timedelta(days=3, seconds=57600),
|
|
"filter_alert": False,
|
|
"filter_alert": False,
|
|
- "water_level": HumidifierWaterLevel.LOW,
|
|
|
|
|
|
+ "water_level": "medium",
|
|
}
|
|
}
|
|
| init_data,
|
|
| init_data,
|
|
"isEncrypted": False,
|
|
"isEncrypted": False,
|
|
@@ -60,143 +70,288 @@ def make_advertisement_data(ble_device: BLEDevice, init_data: dict | None = None
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
-@pytest.mark.asyncio
|
|
|
|
-async def test_process_advertisement():
|
|
|
|
- data = process_evaporative_humidifier(
|
|
|
|
- b"#\x00\x00\x15\x1c\x00",
|
|
|
|
- b"\xd4\x8cIU\x95\xb2\x08\x06\x88\xb3\x90\x81\x00X\x00X2",
|
|
|
|
- )
|
|
|
|
-
|
|
|
|
- assert data == {
|
|
|
|
- "isOn": False,
|
|
|
|
- "mode": None,
|
|
|
|
- "target_humidity": None,
|
|
|
|
- "child_lock": False,
|
|
|
|
- "over_humidify_protection": None,
|
|
|
|
- "tank_removed": False,
|
|
|
|
- "tilted_alert": False,
|
|
|
|
- "filter_missing": False,
|
|
|
|
- "humidity": 51,
|
|
|
|
- "temperature": 16.8,
|
|
|
|
- "filter_run_time": datetime.timedelta(days=3, seconds=57600),
|
|
|
|
- "filter_alert": False,
|
|
|
|
- "water_level": HumidifierWaterLevel.LOW,
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-@pytest.mark.asyncio
|
|
|
|
-async def test_process_advertisement_empty():
|
|
|
|
- data = process_evaporative_humidifier(None, None)
|
|
|
|
-
|
|
|
|
- assert data == {
|
|
|
|
- "isOn": None,
|
|
|
|
- "mode": None,
|
|
|
|
- "target_humidity": None,
|
|
|
|
- "child_lock": None,
|
|
|
|
- "over_humidify_protection": None,
|
|
|
|
- "tank_removed": None,
|
|
|
|
- "tilted_alert": None,
|
|
|
|
- "filter_missing": None,
|
|
|
|
- "humidity": None,
|
|
|
|
- "temperature": None,
|
|
|
|
- "filter_run_time": None,
|
|
|
|
- "filter_alert": None,
|
|
|
|
- "water_level": None,
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
-
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
async def test_turn_on():
|
|
async def test_turn_on():
|
|
- device = create_device_for_command_testing({"isOn": False})
|
|
|
|
- device._send_command = AsyncMock(return_value=b"\x01")
|
|
|
|
-
|
|
|
|
- assert device.is_on() is False
|
|
|
|
|
|
+ """Test the turn_on method."""
|
|
|
|
+ device = create_device_for_command_testing({"isOn": True})
|
|
await device.turn_on()
|
|
await device.turn_on()
|
|
assert device.is_on() is True
|
|
assert device.is_on() is True
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
async def test_turn_off():
|
|
async def test_turn_off():
|
|
- device = create_device_for_command_testing({"isOn": True})
|
|
|
|
- device._send_command = AsyncMock(return_value=b"\x01")
|
|
|
|
-
|
|
|
|
- assert device.is_on() is True
|
|
|
|
|
|
+ """Test the turn_off method."""
|
|
|
|
+ device = create_device_for_command_testing({"isOn": False})
|
|
await device.turn_off()
|
|
await device.turn_off()
|
|
assert device.is_on() is False
|
|
assert device.is_on() is False
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
-async def test_set_mode():
|
|
|
|
- device = create_device_for_command_testing(
|
|
|
|
- {"isOn": True, "mode": HumidifierMode.LOW}
|
|
|
|
- )
|
|
|
|
- device._send_command = AsyncMock(return_value=b"\x01")
|
|
|
|
-
|
|
|
|
- assert device.get_mode() is HumidifierMode.LOW
|
|
|
|
- await device.set_mode(HumidifierMode.AUTO)
|
|
|
|
- assert device.get_mode() is HumidifierMode.AUTO
|
|
|
|
|
|
+async def test_get_basic_is_none():
|
|
|
|
+ """Test the get_basic_info when it returns None."""
|
|
|
|
+ device = create_device_for_command_testing()
|
|
|
|
+ device._get_basic_info = AsyncMock(return_value=None)
|
|
|
|
+ assert await device.get_basic_info() is None
|
|
|
|
|
|
- await device.set_mode(HumidifierMode.TARGET_HUMIDITY, 60)
|
|
|
|
- assert device.get_mode() is HumidifierMode.TARGET_HUMIDITY
|
|
|
|
- assert device.get_target_humidity() == 60
|
|
|
|
|
|
|
|
- await device.set_mode(HumidifierMode.DRYING_FILTER)
|
|
|
|
- assert device.get_mode() is HumidifierMode.DRYING_FILTER
|
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
+ ("basic_info", "result"),
|
|
|
|
+ [
|
|
|
|
+ (
|
|
|
|
+ bytearray(b"\x01\x86\x88\xb1\x98\x82\x00\x1e\x00\x88-\xc4\xff\xff \n\x07"),
|
|
|
|
+ [
|
|
|
|
+ True,
|
|
|
|
+ HumidifierMode(6),
|
|
|
|
+ True,
|
|
|
|
+ False,
|
|
|
|
+ False,
|
|
|
|
+ False,
|
|
|
|
+ False,
|
|
|
|
+ True,
|
|
|
|
+ 49,
|
|
|
|
+ 24.8,
|
|
|
|
+ 24.8,
|
|
|
|
+ 76.64,
|
|
|
|
+ "medium",
|
|
|
|
+ 30,
|
|
|
|
+ 45,
|
|
|
|
+ ],
|
|
|
|
+ ),
|
|
|
|
+ (
|
|
|
|
+ bytearray(b"\x01\x08 \xb1\x98r\x00\x1e\x00\x89-\xc4\xff\xff\x00\x00\x00"),
|
|
|
|
+ [
|
|
|
|
+ False,
|
|
|
|
+ HumidifierMode(8),
|
|
|
|
+ False,
|
|
|
|
+ True,
|
|
|
|
+ False,
|
|
|
|
+ False,
|
|
|
|
+ False,
|
|
|
|
+ True,
|
|
|
|
+ 49,
|
|
|
|
+ 24.7,
|
|
|
|
+ 24.7,
|
|
|
|
+ 76.46,
|
|
|
|
+ "medium",
|
|
|
|
+ 30,
|
|
|
|
+ 45,
|
|
|
|
+ ],
|
|
|
|
+ ),
|
|
|
|
+ ],
|
|
|
|
+)
|
|
|
|
+async def test_get_basic_info(basic_info, result):
|
|
|
|
+ """Test the get_basic_info method."""
|
|
|
|
+ device = create_device_for_command_testing()
|
|
|
|
+ device._get_basic_info = AsyncMock(return_value=basic_info)
|
|
|
|
+
|
|
|
|
+ info = await device.get_basic_info()
|
|
|
|
+ assert info["isOn"] is result[0]
|
|
|
|
+ assert info["mode"] == result[1]
|
|
|
|
+ assert info["over_humidify_protection"] is result[2]
|
|
|
|
+ assert info["child_lock"] is result[3]
|
|
|
|
+ assert info["tank_removed"] is result[4]
|
|
|
|
+ assert info["tilted_alert"] is result[5]
|
|
|
|
+ assert info["filter_missing"] is result[6]
|
|
|
|
+ assert info["is_meter_binded"] is result[7]
|
|
|
|
+ assert info["humidity"] == result[8]
|
|
|
|
+ assert info["temperature"] == result[9]
|
|
|
|
+ assert info["temp"]["c"] == result[10]
|
|
|
|
+ assert info["temp"]["f"] == result[11]
|
|
|
|
+ assert info["water_level"] == result[12]
|
|
|
|
+ assert info["filter_run_time"] == result[13]
|
|
|
|
+ assert info["target_humidity"] == result[14]
|
|
|
|
|
|
- with pytest.raises(ValueError): # noqa: PT011
|
|
|
|
- await device.set_mode(0)
|
|
|
|
|
|
|
|
- with pytest.raises(TypeError):
|
|
|
|
- await device.set_mode(HumidifierMode.TARGET_HUMIDITY)
|
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
+ ("err_msg", "mode", "water_level"),
|
|
|
|
+ [
|
|
|
|
+ (
|
|
|
|
+ "Target humidity can only be set in target humidity mode or sleep mode",
|
|
|
|
+ HumidifierMode.AUTO,
|
|
|
|
+ "low",
|
|
|
|
+ ),
|
|
|
|
+ (
|
|
|
|
+ "Cannot perform operation when water tank is empty",
|
|
|
|
+ HumidifierMode.TARGET_HUMIDITY,
|
|
|
|
+ "empty",
|
|
|
|
+ ),
|
|
|
|
+ ],
|
|
|
|
+)
|
|
|
|
+async def test_set_target_humidity_with_invalid_conditions(err_msg, mode, water_level):
|
|
|
|
+ """Test setting target humidity with invalid mode."""
|
|
|
|
+ device = create_device_for_command_testing()
|
|
|
|
+ device.get_mode = MagicMock(return_value=mode)
|
|
|
|
+ device.get_water_level = MagicMock(return_value=water_level)
|
|
|
|
+ with pytest.raises(SwitchbotOperationError, match=err_msg):
|
|
|
|
+ await device.set_target_humidity(45)
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
-async def test_set_child_lock():
|
|
|
|
- device = create_device_for_command_testing({"child_lock": False})
|
|
|
|
- device._send_command = AsyncMock(return_value=b"\x01")
|
|
|
|
-
|
|
|
|
- assert device.is_child_lock_enabled() is False
|
|
|
|
- await device.set_child_lock(True)
|
|
|
|
- assert device.is_child_lock_enabled() is True
|
|
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
+ ("err_msg", "mode", "water_level", "is_meter_binded", "target_humidity"),
|
|
|
|
+ [
|
|
|
|
+ (
|
|
|
|
+ "Cannot perform operation when water tank is empty",
|
|
|
|
+ HumidifierMode.TARGET_HUMIDITY,
|
|
|
|
+ "empty",
|
|
|
|
+ True,
|
|
|
|
+ 45,
|
|
|
|
+ ),
|
|
|
|
+ (
|
|
|
|
+ "Cannot set target humidity or auto mode when meter is not binded",
|
|
|
|
+ HumidifierMode.TARGET_HUMIDITY,
|
|
|
|
+ "medium",
|
|
|
|
+ False,
|
|
|
|
+ 45,
|
|
|
|
+ ),
|
|
|
|
+ (
|
|
|
|
+ "Target humidity must be set before switching to target humidity mode or sleep mode",
|
|
|
|
+ HumidifierMode.TARGET_HUMIDITY,
|
|
|
|
+ "medium",
|
|
|
|
+ True,
|
|
|
|
+ None,
|
|
|
|
+ ),
|
|
|
|
+ ],
|
|
|
|
+)
|
|
|
|
+async def test_set_mode_with_invalid_conditions(
|
|
|
|
+ err_msg, mode, water_level, is_meter_binded, target_humidity
|
|
|
|
+):
|
|
|
|
+ """Test setting target humidity with invalid mode."""
|
|
|
|
+ device = create_device_for_command_testing()
|
|
|
|
+ device.get_water_level = MagicMock(return_value=water_level)
|
|
|
|
+ device.is_meter_binded = MagicMock(return_value=is_meter_binded)
|
|
|
|
+ device.get_target_humidity = MagicMock(return_value=target_humidity)
|
|
|
|
+ with pytest.raises(SwitchbotOperationError, match=err_msg):
|
|
|
|
+ await device.set_mode(mode)
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
-async def test_start_drying_filter():
|
|
|
|
- device = create_device_for_command_testing(
|
|
|
|
- {"isOn": True, "mode": HumidifierMode.AUTO}
|
|
|
|
- )
|
|
|
|
- device._send_command = AsyncMock(return_value=b"\x01")
|
|
|
|
|
|
+async def test_set_target_humidity():
|
|
|
|
+ """Test setting target humidity."""
|
|
|
|
+ device = create_device_for_command_testing()
|
|
|
|
+ device.get_mode = MagicMock(return_value=HumidifierMode.TARGET_HUMIDITY)
|
|
|
|
|
|
- assert device.get_mode() is HumidifierMode.AUTO
|
|
|
|
- await device.start_drying_filter()
|
|
|
|
- assert device.get_mode() is HumidifierMode.DRYING_FILTER
|
|
|
|
|
|
+ await device.set_target_humidity(45)
|
|
|
|
+ device._send_command.assert_awaited_once_with("570f430202002d")
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
-async def test_stop_drying_filter():
|
|
|
|
- device = create_device_for_command_testing(
|
|
|
|
- {"isOn": True, "mode": HumidifierMode.DRYING_FILTER}
|
|
|
|
- )
|
|
|
|
- device._send_command = AsyncMock(return_value=b"\x00")
|
|
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
+ ("mode", "command"),
|
|
|
|
+ [
|
|
|
|
+ (HumidifierMode.TARGET_HUMIDITY, "570f430202002d"),
|
|
|
|
+ (HumidifierMode.AUTO, "570f4302040000"),
|
|
|
|
+ (HumidifierMode.SLEEP, "570f430203002d"),
|
|
|
|
+ (HumidifierMode.DRYING_FILTER, "570f43010108"),
|
|
|
|
+ ],
|
|
|
|
+)
|
|
|
|
+async def test_set_mode(mode, command):
|
|
|
|
+ """Test setting mode."""
|
|
|
|
+ device = create_device_for_command_testing()
|
|
|
|
+ device.get_target_humidity = MagicMock(return_value=45)
|
|
|
|
|
|
- assert device.is_on() is True
|
|
|
|
- assert device.get_mode() is HumidifierMode.DRYING_FILTER
|
|
|
|
- await device.stop_drying_filter()
|
|
|
|
- assert device.is_on() is False
|
|
|
|
- assert device.get_mode() is None
|
|
|
|
|
|
+ await device.set_mode(mode)
|
|
|
|
+ device._send_command.assert_awaited_once_with(command)
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
-async def test_attributes():
|
|
|
|
- device = create_device_for_command_testing()
|
|
|
|
- device._send_command = AsyncMock(return_value=b"\x01")
|
|
|
|
-
|
|
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
+ ("init_data", "result"),
|
|
|
|
+ [
|
|
|
|
+ (
|
|
|
|
+ {"isOn": False, "mode": HumidifierMode.AUTO},
|
|
|
|
+ [False, HumidifierMode.AUTO, HumidifierAction.OFF],
|
|
|
|
+ ),
|
|
|
|
+ (
|
|
|
|
+ {"isOn": True, "mode": HumidifierMode.TARGET_HUMIDITY},
|
|
|
|
+ [True, HumidifierMode.TARGET_HUMIDITY, HumidifierAction.HUMIDIFYING],
|
|
|
|
+ ),
|
|
|
|
+ (
|
|
|
|
+ {"isOn": True, "mode": HumidifierMode.DRYING_FILTER},
|
|
|
|
+ [True, HumidifierMode.DRYING_FILTER, HumidifierAction.DRYING],
|
|
|
|
+ ),
|
|
|
|
+ ],
|
|
|
|
+)
|
|
|
|
+async def test_status_from_process_adv(init_data, result):
|
|
|
|
+ """Test status from process advertisement."""
|
|
|
|
+ device = create_device_for_command_testing(init_data)
|
|
|
|
+
|
|
|
|
+ assert device.is_on() is result[0]
|
|
|
|
+ assert device.get_mode() is result[1]
|
|
|
|
+ assert device.is_child_lock_enabled() is False
|
|
assert device.is_over_humidify_protection_enabled() is True
|
|
assert device.is_over_humidify_protection_enabled() is True
|
|
assert device.is_tank_removed() is False
|
|
assert device.is_tank_removed() is False
|
|
assert device.is_filter_missing() is False
|
|
assert device.is_filter_missing() is False
|
|
assert device.is_filter_alert_on() is False
|
|
assert device.is_filter_alert_on() is False
|
|
assert device.is_tilted_alert_on() is False
|
|
assert device.is_tilted_alert_on() is False
|
|
- assert device.get_water_level() is HumidifierWaterLevel.LOW
|
|
|
|
|
|
+ assert device.get_water_level() == "medium"
|
|
assert device.get_filter_run_time() == datetime.timedelta(days=3, seconds=57600)
|
|
assert device.get_filter_run_time() == datetime.timedelta(days=3, seconds=57600)
|
|
|
|
+ assert device.get_target_humidity() == 52
|
|
assert device.get_humidity() == 51
|
|
assert device.get_humidity() == 51
|
|
assert device.get_temperature() == 16.8
|
|
assert device.get_temperature() == 16.8
|
|
|
|
+ assert device.get_action() == result[2]
|
|
|
|
+ assert device.is_meter_binded() is True
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
+@pytest.mark.parametrize(
|
|
|
|
+ ("enabled", "command"),
|
|
|
|
+ [
|
|
|
|
+ (True, "570f430501"),
|
|
|
|
+ (False, "570f430500"),
|
|
|
|
+ ],
|
|
|
|
+)
|
|
|
|
+async def test_set_child_lock(enabled, command):
|
|
|
|
+ """Test setting child lock."""
|
|
|
|
+ device = create_device_for_command_testing()
|
|
|
|
+ await device.set_child_lock(enabled)
|
|
|
|
+ device._send_command.assert_awaited_once_with(command)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+@pytest.mark.asyncio
|
|
|
|
+@patch.object(SwitchbotEncryptedDevice, "verify_encryption_key", new_callable=AsyncMock)
|
|
|
|
+async def test_verify_encryption_key(mock_parent_verify):
|
|
|
|
+ ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
|
|
|
|
+ key_id = "ff"
|
|
|
|
+ encryption_key = "ffffffffffffffffffffffffffffffff"
|
|
|
|
+
|
|
|
|
+ mock_parent_verify.return_value = True
|
|
|
|
+
|
|
|
|
+ result = await evaporative_humidifier.SwitchbotEvaporativeHumidifier.verify_encryption_key(
|
|
|
|
+ device=ble_device,
|
|
|
|
+ key_id=key_id,
|
|
|
|
+ encryption_key=encryption_key,
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ mock_parent_verify.assert_awaited_once_with(
|
|
|
|
+ ble_device,
|
|
|
|
+ key_id,
|
|
|
|
+ encryption_key,
|
|
|
|
+ SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ assert result is True
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def test_evaporative_humidifier_modes():
|
|
|
|
+ assert HumidifierMode.get_modes() == [
|
|
|
|
+ "high",
|
|
|
|
+ "medium",
|
|
|
|
+ "low",
|
|
|
|
+ "quiet",
|
|
|
|
+ "target_humidity",
|
|
|
|
+ "sleep",
|
|
|
|
+ "auto",
|
|
|
|
+ "drying_filter",
|
|
|
|
+ ]
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def test_evaporative_humidifier_water_levels():
|
|
|
|
+ assert HumidifierWaterLevel.get_levels() == [
|
|
|
|
+ "empty",
|
|
|
|
+ "low",
|
|
|
|
+ "medium",
|
|
|
|
+ "high",
|
|
|
|
+ ]
|