Adjust litterrobot tests and code to match guidelines (#47060)

* Use SwitchEntity instead of ToggleEntity and adjust test patches as recommended

* Move async_create_entry out of try block in config_flow

* Patch pypi package instead of HA code

* Bump pylitterbot to 2021.2.6, fix tests, and implement other code review suggestions

* Bump pylitterbot to 2021.2.8, remove sleep mode start/end time from vacuum, adjust and add sensors for sleep mode start/end time

* Move icon helper back to Litter-Robot component and isoformat times on time sensors
This commit is contained in:
Nathan Spencer 2021-03-06 09:28:33 -07:00 committed by GitHub
parent 14f85d8731
commit e9052233a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 277 additions and 142 deletions

View File

@ -35,9 +35,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
hub = LitterRobotHub(self.hass, user_input) hub = LitterRobotHub(self.hass, user_input)
try: try:
await hub.login() await hub.login()
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
except LitterRobotLoginException: except LitterRobotLoginException:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except LitterRobotException: except LitterRobotException:
@ -46,6 +43,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
if not errors:
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
) )

View File

@ -4,7 +4,7 @@ import logging
from types import MethodType from types import MethodType
from typing import Any, Optional from typing import Any, Optional
from pylitterbot import Account, Robot import pylitterbot
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@ -49,7 +49,7 @@ class LitterRobotHub:
async def login(self, load_robots: bool = False): async def login(self, load_robots: bool = False):
"""Login to Litter-Robot.""" """Login to Litter-Robot."""
self.logged_in = False self.logged_in = False
self.account = Account() self.account = pylitterbot.Account()
try: try:
await self.account.connect( await self.account.connect(
username=self._data[CONF_USERNAME], username=self._data[CONF_USERNAME],
@ -69,11 +69,11 @@ class LitterRobotHub:
class LitterRobotEntity(CoordinatorEntity): class LitterRobotEntity(CoordinatorEntity):
"""Generic Litter-Robot entity representing common data and methods.""" """Generic Litter-Robot entity representing common data and methods."""
def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub): def __init__(self, robot: pylitterbot.Robot, entity_type: str, hub: LitterRobotHub):
"""Pass coordinator to CoordinatorEntity.""" """Pass coordinator to CoordinatorEntity."""
super().__init__(hub.coordinator) super().__init__(hub.coordinator)
self.robot = robot self.robot = robot
self.entity_type = entity_type if entity_type else "" self.entity_type = entity_type
self.hub = hub self.hub = hub
@property @property
@ -89,22 +89,21 @@ class LitterRobotEntity(CoordinatorEntity):
@property @property
def device_info(self): def device_info(self):
"""Return the device information for a Litter-Robot.""" """Return the device information for a Litter-Robot."""
model = "Litter-Robot 3 Connect"
if not self.robot.serial.startswith("LR3C"):
model = "Other Litter-Robot Connected Device"
return { return {
"identifiers": {(DOMAIN, self.robot.serial)}, "identifiers": {(DOMAIN, self.robot.serial)},
"name": self.robot.name, "name": self.robot.name,
"manufacturer": "Litter-Robot", "manufacturer": "Litter-Robot",
"model": model, "model": self.robot.model,
} }
async def perform_action_and_refresh(self, action: MethodType, *args: Any): async def perform_action_and_refresh(self, action: MethodType, *args: Any):
"""Perform an action and initiates a refresh of the robot data after a few seconds.""" """Perform an action and initiates a refresh of the robot data after a few seconds."""
async def async_call_later_callback(*_) -> None:
await self.hub.coordinator.async_request_refresh()
await action(*args) await action(*args)
async_call_later( async_call_later(self.hass, REFRESH_WAIT_TIME, async_call_later_callback)
self.hass, REFRESH_WAIT_TIME, self.hub.coordinator.async_request_refresh
)
@staticmethod @staticmethod
def parse_time_at_default_timezone(time_str: str) -> Optional[time]: def parse_time_at_default_timezone(time_str: str) -> Optional[time]:

View File

@ -3,6 +3,6 @@
"name": "Litter-Robot", "name": "Litter-Robot",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot", "documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2021.2.5"], "requirements": ["pylitterbot==2021.2.8"],
"codeowners": ["@natekspencer"] "codeowners": ["@natekspencer"]
} }

View File

@ -1,32 +1,44 @@
"""Support for Litter-Robot sensors.""" """Support for Litter-Robot sensors."""
from homeassistant.const import PERCENTAGE from typing import Optional
from pylitterbot.robot import Robot
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN
from .hub import LitterRobotEntity from .hub import LitterRobotEntity, LitterRobotHub
WASTE_DRAWER = "Waste Drawer"
async def async_setup_entry(hass, config_entry, async_add_entities): def icon_for_gauge_level(gauge_level: Optional[int] = None, offset: int = 0) -> str:
"""Set up Litter-Robot sensors using config entry.""" """Return a gauge icon valid identifier."""
hub = hass.data[DOMAIN][config_entry.entry_id] if gauge_level is None or gauge_level <= 0 + offset:
return "mdi:gauge-empty"
entities = [] if gauge_level > 70 + offset:
for robot in hub.account.robots: return "mdi:gauge-full"
entities.append(LitterRobotSensor(robot, WASTE_DRAWER, hub)) if gauge_level > 30 + offset:
return "mdi:gauge"
if entities: return "mdi:gauge-low"
async_add_entities(entities, True)
class LitterRobotSensor(LitterRobotEntity, Entity): class LitterRobotPropertySensor(LitterRobotEntity, Entity):
"""Litter-Robot sensors.""" """Litter-Robot property sensors."""
def __init__(
self, robot: Robot, entity_type: str, hub: LitterRobotHub, sensor_attribute: str
):
"""Pass coordinator to CoordinatorEntity."""
super().__init__(robot, entity_type, hub)
self.sensor_attribute = sensor_attribute
@property @property
def state(self): def state(self):
"""Return the state.""" """Return the state."""
return self.robot.waste_drawer_gauge return getattr(self.robot, self.sensor_attribute)
class LitterRobotWasteSensor(LitterRobotPropertySensor, Entity):
"""Litter-Robot sensors."""
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -36,19 +48,40 @@ class LitterRobotSensor(LitterRobotEntity, Entity):
@property @property
def icon(self): def icon(self):
"""Return the icon to use in the frontend, if any.""" """Return the icon to use in the frontend, if any."""
if self.robot.waste_drawer_gauge <= 10: return icon_for_gauge_level(self.state, 10)
return "mdi:gauge-empty"
if self.robot.waste_drawer_gauge < 50:
return "mdi:gauge-low" class LitterRobotSleepTimeSensor(LitterRobotPropertySensor, Entity):
if self.robot.waste_drawer_gauge <= 90: """Litter-Robot sleep time sensors."""
return "mdi:gauge"
return "mdi:gauge-full"
@property @property
def device_state_attributes(self): def state(self):
"""Return device specific state attributes.""" """Return the state."""
return { if self.robot.sleep_mode_active:
"cycle_count": self.robot.cycle_count, return super().state.isoformat()
"cycle_capacity": self.robot.cycle_capacity, return None
"cycles_after_drawer_full": self.robot.cycles_after_drawer_full,
} @property
def device_class(self):
"""Return the device class, if any."""
return DEVICE_CLASS_TIMESTAMP
ROBOT_SENSORS = [
(LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_gauge"),
(LitterRobotSleepTimeSensor, "Sleep Mode Start Time", "sleep_mode_start_time"),
(LitterRobotSleepTimeSensor, "Sleep Mode End Time", "sleep_mode_end_time"),
]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Litter-Robot sensors using config entry."""
hub = hass.data[DOMAIN][config_entry.entry_id]
entities = []
for robot in hub.account.robots:
for (sensor_class, entity_type, sensor_attribute) in ROBOT_SENSORS:
entities.append(sensor_class(robot, entity_type, hub, sensor_attribute))
if entities:
async_add_entities(entities, True)

View File

@ -1,11 +1,11 @@
"""Support for Litter-Robot switches.""" """Support for Litter-Robot switches."""
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import SwitchEntity
from .const import DOMAIN from .const import DOMAIN
from .hub import LitterRobotEntity from .hub import LitterRobotEntity
class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity): class LitterRobotNightLightModeSwitch(LitterRobotEntity, SwitchEntity):
"""Litter-Robot Night Light Mode Switch.""" """Litter-Robot Night Light Mode Switch."""
@property @property
@ -27,7 +27,7 @@ class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity):
await self.perform_action_and_refresh(self.robot.set_night_light, False) await self.perform_action_and_refresh(self.robot.set_night_light, False)
class LitterRobotPanelLockoutSwitch(LitterRobotEntity, ToggleEntity): class LitterRobotPanelLockoutSwitch(LitterRobotEntity, SwitchEntity):
"""Litter-Robot Panel Lockout Switch.""" """Litter-Robot Panel Lockout Switch."""
@property @property

View File

@ -14,7 +14,6 @@ from homeassistant.components.vacuum import (
VacuumEntity, VacuumEntity,
) )
from homeassistant.const import STATE_OFF from homeassistant.const import STATE_OFF
import homeassistant.util.dt as dt_util
from .const import DOMAIN from .const import DOMAIN
from .hub import LitterRobotEntity from .hub import LitterRobotEntity
@ -54,27 +53,22 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity):
def state(self): def state(self):
"""Return the state of the cleaner.""" """Return the state of the cleaner."""
switcher = { switcher = {
Robot.UnitStatus.CCP: STATE_CLEANING, Robot.UnitStatus.CLEAN_CYCLE: STATE_CLEANING,
Robot.UnitStatus.EC: STATE_CLEANING, Robot.UnitStatus.EMPTY_CYCLE: STATE_CLEANING,
Robot.UnitStatus.CCC: STATE_DOCKED, Robot.UnitStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
Robot.UnitStatus.CST: STATE_DOCKED, Robot.UnitStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
Robot.UnitStatus.DF1: STATE_DOCKED, Robot.UnitStatus.DRAWER_FULL_1: STATE_DOCKED,
Robot.UnitStatus.DF2: STATE_DOCKED, Robot.UnitStatus.DRAWER_FULL_2: STATE_DOCKED,
Robot.UnitStatus.RDY: STATE_DOCKED, Robot.UnitStatus.READY: STATE_DOCKED,
Robot.UnitStatus.OFF: STATE_OFF, Robot.UnitStatus.OFF: STATE_OFF,
} }
return switcher.get(self.robot.unit_status, STATE_ERROR) return switcher.get(self.robot.unit_status, STATE_ERROR)
@property
def error(self):
"""Return the error associated with the current state, if any."""
return self.robot.unit_status.value
@property @property
def status(self): def status(self):
"""Return the status of the cleaner.""" """Return the status of the cleaner."""
return f"{self.robot.unit_status.value}{' (Sleeping)' if self.robot.is_sleeping else ''}" return f"{self.robot.unit_status.label}{' (Sleeping)' if self.robot.is_sleeping else ''}"
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the cleaner on, starting a clean cycle.""" """Turn the cleaner on, starting a clean cycle."""
@ -119,22 +113,11 @@ class LitterRobotCleaner(LitterRobotEntity, VacuumEntity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
[sleep_mode_start_time, sleep_mode_end_time] = [None, None]
if self.robot.sleep_mode_active:
sleep_mode_start_time = dt_util.as_local(
self.robot.sleep_mode_start_time
).strftime("%H:%M:00")
sleep_mode_end_time = dt_util.as_local(
self.robot.sleep_mode_end_time
).strftime("%H:%M:00")
return { return {
"clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes,
"is_sleeping": self.robot.is_sleeping, "is_sleeping": self.robot.is_sleeping,
"sleep_mode_start_time": sleep_mode_start_time, "sleep_mode_active": self.robot.sleep_mode_active,
"sleep_mode_end_time": sleep_mode_end_time,
"power_status": self.robot.power_status, "power_status": self.robot.power_status,
"unit_status_code": self.robot.unit_status.name, "unit_status_code": self.robot.unit_status.value,
"last_seen": self.robot.last_seen, "last_seen": self.robot.last_seen,
} }

View File

@ -1501,7 +1501,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2021.2.5 pylitterbot==2021.2.8
# homeassistant.components.loopenergy # homeassistant.components.loopenergy
pyloopenergy==0.2.1 pyloopenergy==0.2.1

View File

@ -791,7 +791,7 @@ pylibrespot-java==0.1.0
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2021.2.5 pylitterbot==2021.2.8
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.9.0 pylutron-caseta==0.9.0

View File

@ -1,45 +1,59 @@
"""Configure pytest for Litter-Robot tests.""" """Configure pytest for Litter-Robot tests."""
from typing import Optional
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pylitterbot
from pylitterbot import Robot from pylitterbot import Robot
import pytest import pytest
from homeassistant.components import litterrobot from homeassistant.components import litterrobot
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .common import CONFIG, ROBOT_DATA from .common import CONFIG, ROBOT_DATA
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
def create_mock_robot(hass): def create_mock_robot(unit_status_code: Optional[str] = None):
"""Create a mock Litter-Robot device.""" """Create a mock Litter-Robot device."""
robot = Robot(data=ROBOT_DATA) if not (
robot.start_cleaning = AsyncMock() unit_status_code
robot.set_power_status = AsyncMock() and Robot.UnitStatus(unit_status_code) != Robot.UnitStatus.UNKNOWN
robot.reset_waste_drawer = AsyncMock() ):
robot.set_sleep_mode = AsyncMock() unit_status_code = ROBOT_DATA["unitStatus"]
robot.set_night_light = AsyncMock()
robot.set_panel_lockout = AsyncMock() with patch.dict(ROBOT_DATA, {"unitStatus": unit_status_code}):
return robot robot = Robot(data=ROBOT_DATA)
robot.start_cleaning = AsyncMock()
robot.set_power_status = AsyncMock()
robot.reset_waste_drawer = AsyncMock()
robot.set_sleep_mode = AsyncMock()
robot.set_night_light = AsyncMock()
robot.set_panel_lockout = AsyncMock()
return robot
@pytest.fixture() def create_mock_account(unit_status_code: Optional[str] = None):
def mock_hub(hass): """Create a mock Litter-Robot account."""
"""Mock a Litter-Robot hub.""" account = MagicMock(spec=pylitterbot.Account)
hub = MagicMock( account.connect = AsyncMock()
hass=hass, account.refresh_robots = AsyncMock()
account=MagicMock(), account.robots = [create_mock_robot(unit_status_code)]
logged_in=True, return account
coordinator=MagicMock(spec=DataUpdateCoordinator),
spec=litterrobot.LitterRobotHub,
)
hub.coordinator.last_update_success = True
hub.account.robots = [create_mock_robot(hass)]
return hub
async def setup_hub(hass, mock_hub, platform_domain): @pytest.fixture
def mock_account():
"""Mock a Litter-Robot account."""
return create_mock_account()
@pytest.fixture
def mock_account_with_error():
"""Mock a Litter-Robot account with error."""
return create_mock_account("BR")
async def setup_integration(hass, mock_account, platform_domain=None):
"""Load a Litter-Robot platform with the provided hub.""" """Load a Litter-Robot platform with the provided hub."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=litterrobot.DOMAIN, domain=litterrobot.DOMAIN,
@ -47,9 +61,11 @@ async def setup_hub(hass, mock_hub, platform_domain):
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch( with patch("pylitterbot.Account", return_value=mock_account), patch(
"homeassistant.components.litterrobot.LitterRobotHub", "homeassistant.components.litterrobot.PLATFORMS",
return_value=mock_hub, [platform_domain] if platform_domain else [],
): ):
await hass.config_entries.async_forward_entry_setup(entry, platform_domain) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
return entry

View File

@ -4,11 +4,14 @@ from unittest.mock import patch
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
from homeassistant import config_entries, setup from homeassistant import config_entries, setup
from homeassistant.components import litterrobot
from .common import CONF_USERNAME, CONFIG, DOMAIN from .common import CONF_USERNAME, CONFIG, DOMAIN
from tests.common import MockConfigEntry
async def test_form(hass):
async def test_form(hass, mock_account):
"""Test we get the form.""" """Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -17,10 +20,7 @@ async def test_form(hass):
assert result["type"] == "form" assert result["type"] == "form"
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch("pylitterbot.Account", return_value=mock_account), patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login",
return_value=True,
), patch(
"homeassistant.components.litterrobot.async_setup", return_value=True "homeassistant.components.litterrobot.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
"homeassistant.components.litterrobot.async_setup_entry", "homeassistant.components.litterrobot.async_setup_entry",
@ -38,6 +38,23 @@ async def test_form(hass):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_already_configured(hass):
"""Test we handle already configured."""
MockConfigEntry(
domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN],
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data=CONFIG[litterrobot.DOMAIN],
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_form_invalid_auth(hass): async def test_form_invalid_auth(hass):
"""Test we handle invalid auth.""" """Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -45,7 +62,7 @@ async def test_form_invalid_auth(hass):
) )
with patch( with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", "pylitterbot.Account.connect",
side_effect=LitterRobotLoginException, side_effect=LitterRobotLoginException,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -63,7 +80,7 @@ async def test_form_cannot_connect(hass):
) )
with patch( with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", "pylitterbot.Account.connect",
side_effect=LitterRobotException, side_effect=LitterRobotException,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@ -81,7 +98,7 @@ async def test_form_unknown_error(hass):
) )
with patch( with patch(
"homeassistant.components.litterrobot.config_flow.LitterRobotHub.login", "pylitterbot.Account.connect",
side_effect=Exception, side_effect=Exception,
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(

View File

@ -1,20 +1,48 @@
"""Test Litter-Robot setup process.""" """Test Litter-Robot setup process."""
from unittest.mock import patch
from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException
import pytest
from homeassistant.components import litterrobot from homeassistant.components import litterrobot
from homeassistant.setup import async_setup_component from homeassistant.config_entries import (
ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
)
from .common import CONFIG from .common import CONFIG
from .conftest import setup_integration
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_unload_entry(hass): async def test_unload_entry(hass, mock_account):
"""Test being able to unload an entry.""" """Test being able to unload an entry."""
entry = await setup_integration(hass, mock_account)
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert hass.data[litterrobot.DOMAIN] == {}
@pytest.mark.parametrize(
"side_effect,expected_state",
(
(LitterRobotLoginException, ENTRY_STATE_SETUP_ERROR),
(LitterRobotException, ENTRY_STATE_SETUP_RETRY),
),
)
async def test_entry_not_setup(hass, side_effect, expected_state):
"""Test being able to handle config entry not setup."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=litterrobot.DOMAIN, domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN], data=CONFIG[litterrobot.DOMAIN],
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
assert await async_setup_component(hass, litterrobot.DOMAIN, {}) is True with patch(
assert await litterrobot.async_unload_entry(hass, entry) "pylitterbot.Account.connect",
assert hass.data[litterrobot.DOMAIN] == {} side_effect=side_effect,
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state == expected_state

View File

@ -1,20 +1,57 @@
"""Test the Litter-Robot sensor entity.""" """Test the Litter-Robot sensor entity."""
from unittest.mock import Mock
from homeassistant.components.litterrobot.sensor import LitterRobotSleepTimeSensor
from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import PERCENTAGE from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
from .conftest import setup_hub from .conftest import create_mock_robot, setup_integration
ENTITY_ID = "sensor.test_waste_drawer" WASTE_DRAWER_ENTITY_ID = "sensor.test_waste_drawer"
async def test_sensor(hass, mock_hub): async def test_waste_drawer_sensor(hass, mock_account):
"""Tests the sensor entity was set up.""" """Tests the waste drawer sensor entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
sensor = hass.states.get(ENTITY_ID) sensor = hass.states.get(WASTE_DRAWER_ENTITY_ID)
assert sensor assert sensor
assert sensor.state == "50" assert sensor.state == "50"
assert sensor.attributes["cycle_count"] == 15
assert sensor.attributes["cycle_capacity"] == 30
assert sensor.attributes["cycles_after_drawer_full"] == 0
assert sensor.attributes["unit_of_measurement"] == PERCENTAGE assert sensor.attributes["unit_of_measurement"] == PERCENTAGE
async def test_sleep_time_sensor_with_none_state(hass):
"""Tests the sleep mode start time sensor where sleep mode is inactive."""
robot = create_mock_robot()
robot.sleep_mode_active = False
sensor = LitterRobotSleepTimeSensor(
robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time"
)
assert sensor
assert sensor.state is None
assert sensor.device_class == DEVICE_CLASS_TIMESTAMP
async def test_gauge_icon():
"""Test icon generator for gauge sensor."""
from homeassistant.components.litterrobot.sensor import icon_for_gauge_level
GAUGE_EMPTY = "mdi:gauge-empty"
GAUGE_LOW = "mdi:gauge-low"
GAUGE = "mdi:gauge"
GAUGE_FULL = "mdi:gauge-full"
assert icon_for_gauge_level(None) == GAUGE_EMPTY
assert icon_for_gauge_level(0) == GAUGE_EMPTY
assert icon_for_gauge_level(5) == GAUGE_LOW
assert icon_for_gauge_level(40) == GAUGE
assert icon_for_gauge_level(80) == GAUGE_FULL
assert icon_for_gauge_level(100) == GAUGE_FULL
assert icon_for_gauge_level(None, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(0, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(5, 10) == GAUGE_EMPTY
assert icon_for_gauge_level(40, 10) == GAUGE_LOW
assert icon_for_gauge_level(80, 10) == GAUGE
assert icon_for_gauge_level(100, 10) == GAUGE_FULL

View File

@ -12,7 +12,7 @@ from homeassistant.components.switch import (
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .conftest import setup_hub from .conftest import setup_integration
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@ -20,9 +20,9 @@ NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode"
PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout" PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout"
async def test_switch(hass, mock_hub): async def test_switch(hass, mock_account):
"""Tests the switch entity was set up.""" """Tests the switch entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID) switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID)
assert switch assert switch
@ -36,9 +36,9 @@ async def test_switch(hass, mock_hub):
(PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"), (PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"),
], ],
) )
async def test_on_off_commands(hass, mock_hub, entity_id, robot_command): async def test_on_off_commands(hass, mock_account, entity_id, robot_command):
"""Test sending commands to the switch.""" """Test sending commands to the switch."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
switch = hass.states.get(entity_id) switch = hass.states.get(entity_id)
assert switch assert switch
@ -48,12 +48,14 @@ async def test_on_off_commands(hass, mock_hub, entity_id, robot_command):
count = 0 count = 0
for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]: for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]:
count += 1 count += 1
await hass.services.async_call( await hass.services.async_call(
PLATFORM_DOMAIN, PLATFORM_DOMAIN,
service, service,
data, data,
blocking=True, blocking=True,
) )
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
assert getattr(mock_hub.account.robots[0], robot_command).call_count == count assert getattr(mock_account.robots[0], robot_command).call_count == count

View File

@ -12,20 +12,21 @@ from homeassistant.components.vacuum import (
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_DOCKED, STATE_DOCKED,
STATE_ERROR,
) )
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .conftest import setup_hub from .conftest import setup_integration
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
ENTITY_ID = "vacuum.test_litter_box" ENTITY_ID = "vacuum.test_litter_box"
async def test_vacuum(hass, mock_hub): async def test_vacuum(hass, mock_account):
"""Tests the vacuum entity was set up.""" """Tests the vacuum entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID) vacuum = hass.states.get(ENTITY_ID)
assert vacuum assert vacuum
@ -33,6 +34,15 @@ async def test_vacuum(hass, mock_hub):
assert vacuum.attributes["is_sleeping"] is False assert vacuum.attributes["is_sleeping"] is False
async def test_vacuum_with_error(hass, mock_account_with_error):
"""Tests a vacuum entity with an error."""
await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID)
assert vacuum
assert vacuum.state == STATE_ERROR
@pytest.mark.parametrize( @pytest.mark.parametrize(
"service,command,extra", "service,command,extra",
[ [
@ -52,14 +62,22 @@ async def test_vacuum(hass, mock_hub):
ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"}, ATTR_PARAMS: {"enabled": True, "sleep_time": "22:30"},
}, },
), ),
(
SERVICE_SEND_COMMAND,
"set_sleep_mode",
{
ATTR_COMMAND: "set_sleep_mode",
ATTR_PARAMS: {"enabled": True, "sleep_time": None},
},
),
], ],
) )
async def test_commands(hass, mock_hub, service, command, extra): async def test_commands(hass, mock_account, service, command, extra):
"""Test sending commands to the vacuum.""" """Test sending commands to the vacuum."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN) await setup_integration(hass, mock_account, PLATFORM_DOMAIN)
vacuum = hass.states.get(ENTITY_ID) vacuum = hass.states.get(ENTITY_ID)
assert vacuum is not None assert vacuum
assert vacuum.state == STATE_DOCKED assert vacuum.state == STATE_DOCKED
data = {ATTR_ENTITY_ID: ENTITY_ID} data = {ATTR_ENTITY_ID: ENTITY_ID}
@ -74,4 +92,4 @@ async def test_commands(hass, mock_hub, service, command, extra):
) )
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME) future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
getattr(mock_hub.account.robots[0], command).assert_called_once() getattr(mock_account.robots[0], command).assert_called_once()