Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions switchbot/devices/humidifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,38 +49,40 @@ async def _async_set_state(self, state: bool) -> bool:
level = self.get_target_humidity() or 128
result = await self._send_command(self._generate_command(on=state, level=level))
ret = self._check_command_result(result, 0, {0x01})
self._override_state({"isOn": state, "level": level})
self._fire_callbacks()
if ret:
self._override_state({"isOn": state, "level": level})
self._fire_callbacks()
return ret

async def turn_on(self) -> bool:
"""Turn device on."""
await self._async_set_state(True)
return await self._async_set_state(True)

async def turn_off(self) -> bool:
"""Turn device off."""
await self._async_set_state(False)
return await self._async_set_state(False)

async def set_level(self, level: int) -> bool:
"""Set level."""
assert 1 <= level <= 100, "Level must be between 1 and 100"
await self._set_level(level)
return await self._set_level(level)

async def _set_level(self, level: int) -> bool:
"""Set level."""
result = await self._send_command(self._generate_command(level=level))
ret = self._check_command_result(result, 0, {0x01})
self._override_state({"level": level})
self._fire_callbacks()
if ret:
self._override_state({"level": level})
self._fire_callbacks()
return ret

async def async_set_auto(self) -> bool:
"""Set auto mode."""
await self._set_level(128)
return await self._set_level(128)

async def async_set_manual(self) -> bool:
"""Set manual mode."""
await self._set_level(50)
return await self._set_level(50)

def is_auto(self) -> bool:
"""Return auto state from cache."""
Expand Down
10 changes: 6 additions & 4 deletions switchbot/devices/plug.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ async def turn_on(self) -> bool:
"""Turn device on."""
result = await self._send_command(PLUG_ON_KEY)
ret = self._check_command_result(result, 1, {0x80})
self._override_state({"isOn": True})
self._fire_callbacks()
if ret:
self._override_state({"isOn": True})
self._fire_callbacks()
return ret

async def turn_off(self) -> bool:
"""Turn device off."""
result = await self._send_command(PLUG_OFF_KEY)
ret = self._check_command_result(result, 1, {0x80})
self._override_state({"isOn": False})
self._fire_callbacks()
if ret:
self._override_state({"isOn": False})
self._fire_callbacks()
return ret

def is_on(self) -> bool | None:
Expand Down
134 changes: 134 additions & 0 deletions tests/test_humidifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from unittest.mock import AsyncMock, MagicMock

import pytest
from bleak.backends.device import BLEDevice

from switchbot import SwitchBotAdvertisement, SwitchbotModel
from switchbot.devices import humidifier

from .test_adv_parser import generate_ble_device


def create_humidifier_for_command_testing(
init_data: dict | None = None,
) -> humidifier.SwitchbotHumidifier:
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
device = humidifier.SwitchbotHumidifier(ble_device, model=SwitchbotModel.HUMIDIFIER)
device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
device._send_command = AsyncMock()
device._check_command_result = MagicMock(return_value=True)
return device


def make_advertisement_data(
ble_device: BLEDevice, init_data: dict | None = None
) -> SwitchBotAdvertisement:
if init_data is None:
init_data = {}
return SwitchBotAdvertisement(
address="aa:bb:cc:dd:ee:ff",
data={
"rawAdvData": b"e\x00\x00",
"data": {"isOn": False, "level": 50} | init_data,
"model": "e",
"isEncrypted": False,
"modelFriendlyName": "Humidifier",
"modelName": SwitchbotModel.HUMIDIFIER,
},
device=ble_device,
rssi=-65,
active=True,
)


@pytest.mark.asyncio
async def test_turn_on_accepted_overrides_state() -> None:
"""Accepted command must update cached state to on."""
device = create_humidifier_for_command_testing({"isOn": False, "level": 50})

assert await device.turn_on() is True

device._send_command.assert_awaited_once()
assert device.is_on() is True


@pytest.mark.asyncio
async def test_turn_off_accepted_overrides_state() -> None:
"""Accepted command must update cached state to off."""
device = create_humidifier_for_command_testing({"isOn": True, "level": 50})

assert await device.turn_off() is True

device._send_command.assert_awaited_once()
assert device.is_on() is False


@pytest.mark.asyncio
async def test_turn_on_rejected_preserves_state() -> None:
"""Rejected command must NOT flip cached isOn to True."""
device = create_humidifier_for_command_testing({"isOn": False, "level": 50})
device._check_command_result = MagicMock(return_value=False)

assert await device.turn_on() is False

device._send_command.assert_awaited_once()
assert device.is_on() is False


@pytest.mark.asyncio
async def test_turn_off_rejected_preserves_state() -> None:
"""Rejected command must NOT flip cached isOn to False."""
device = create_humidifier_for_command_testing({"isOn": True, "level": 50})
device._check_command_result = MagicMock(return_value=False)

assert await device.turn_off() is False

device._send_command.assert_awaited_once()
assert device.is_on() is True


@pytest.mark.asyncio
async def test_set_level_accepted_overrides_state() -> None:
"""Accepted set_level must update cached level."""
device = create_humidifier_for_command_testing({"isOn": True, "level": 33})

assert await device.set_level(66) is True

device._send_command.assert_awaited_once()
assert device.get_level() == 66


@pytest.mark.asyncio
async def test_set_level_rejected_preserves_state() -> None:
"""Rejected set_level must NOT update cached level."""
device = create_humidifier_for_command_testing({"isOn": True, "level": 33})
device._check_command_result = MagicMock(return_value=False)

assert await device.set_level(66) is False

device._send_command.assert_awaited_once()
assert device.get_level() == 33


@pytest.mark.asyncio
async def test_async_set_auto_overrides_level() -> None:
"""Auto mode must set the cached level to the auto sentinel."""
device = create_humidifier_for_command_testing({"isOn": True, "level": 50})

assert await device.async_set_auto() is True

device._send_command.assert_awaited_once()
assert device.get_level() == 128
assert device.is_auto() is True


@pytest.mark.asyncio
async def test_async_set_manual_overrides_level() -> None:
"""Manual mode must set the cached level to the manual sentinel."""
device = create_humidifier_for_command_testing({"isOn": True, "level": 128})

assert await device.async_set_manual() is True

device._send_command.assert_awaited_once()
assert device.get_level() == 50
assert device.is_auto() is False
87 changes: 87 additions & 0 deletions tests/test_plug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from unittest.mock import AsyncMock, MagicMock

import pytest
from bleak.backends.device import BLEDevice

from switchbot import SwitchBotAdvertisement, SwitchbotModel
from switchbot.devices import plug

from .test_adv_parser import generate_ble_device


def create_plug_for_command_testing(
init_data: dict | None = None,
) -> plug.SwitchbotPlugMini:
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
device = plug.SwitchbotPlugMini(ble_device, model=SwitchbotModel.PLUG_MINI)
device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
device._send_command = AsyncMock()
device._check_command_result = MagicMock(return_value=True)
return device


def make_advertisement_data(
ble_device: BLEDevice, init_data: dict | None = None
) -> SwitchBotAdvertisement:
if init_data is None:
init_data = {}
return SwitchBotAdvertisement(
address="aa:bb:cc:dd:ee:ff",
data={
"rawAdvData": b"g\x00\x00",
"data": {"switchMode": True, "isOn": False, "wattage": 0} | init_data,
"model": "g",
"isEncrypted": False,
"modelFriendlyName": "Plug Mini",
"modelName": SwitchbotModel.PLUG_MINI,
},
device=ble_device,
rssi=-65,
active=True,
)


@pytest.mark.asyncio
async def test_turn_on_accepted_overrides_state() -> None:
"""Accepted command must update cached state to on."""
device = create_plug_for_command_testing({"isOn": False})

assert await device.turn_on() is True

device._send_command.assert_awaited_once_with(plug.PLUG_ON_KEY)
assert device.is_on() is True


@pytest.mark.asyncio
async def test_turn_off_accepted_overrides_state() -> None:
"""Accepted command must update cached state to off."""
device = create_plug_for_command_testing({"isOn": True})

assert await device.turn_off() is True

device._send_command.assert_awaited_once_with(plug.PLUG_OFF_KEY)
assert device.is_on() is False


@pytest.mark.asyncio
async def test_turn_on_rejected_preserves_state() -> None:
"""Rejected command must NOT override the cached state."""
device = create_plug_for_command_testing({"isOn": False})
device._check_command_result = MagicMock(return_value=False)

assert await device.turn_on() is False

device._send_command.assert_awaited_once_with(plug.PLUG_ON_KEY)
assert device.is_on() is False


@pytest.mark.asyncio
async def test_turn_off_rejected_preserves_state() -> None:
"""Rejected command must NOT override the cached state to off."""
device = create_plug_for_command_testing({"isOn": True})
device._check_command_result = MagicMock(return_value=False)

assert await device.turn_off() is False

device._send_command.assert_awaited_once_with(plug.PLUG_OFF_KEY)
assert device.is_on() is True
Loading