Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion homeassistant/components/litterrobot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .const import DOMAIN
from .hub import LitterRobotHub

PLATFORMS = ["vacuum"]
PLATFORMS = ["sensor", "switch", "vacuum"]


async def async_setup(hass: HomeAssistant, config: dict):
Expand Down
54 changes: 54 additions & 0 deletions homeassistant/components/litterrobot/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Support for Litter-Robot sensors."""
from homeassistant.const import PERCENTAGE
from homeassistant.helpers.entity import Entity

from .const import DOMAIN
from .hub import LitterRobotEntity

WASTE_DRAWER = "Waste Drawer"


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:
entities.append(LitterRobotSensor(robot, WASTE_DRAWER, hub))

if entities:
async_add_entities(entities, True)


class LitterRobotSensor(LitterRobotEntity, Entity):
"""Litter-Robot sensors."""

@property
def state(self):
"""Return the state."""
return self.robot.waste_drawer_gauge

@property
def unit_of_measurement(self):
"""Return unit of measurement."""
return PERCENTAGE

@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
if self.robot.waste_drawer_gauge <= 10:
return "mdi:gauge-empty"
if self.robot.waste_drawer_gauge < 50:
return "mdi:gauge-low"
if self.robot.waste_drawer_gauge <= 90:
return "mdi:gauge"
return "mdi:gauge-full"

@property
def device_state_attributes(self):
"""Return device specific state attributes."""
return {
"cycle_count": self.robot.cycle_count,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these attributes be separate sensor entities instead? If a measurement is relevant on its own we want it to be a separate entity.

"cycle_capacity": self.robot.cycle_capacity,
"cycles_after_drawer_full": self.robot.cycles_after_drawer_full,
}
68 changes: 68 additions & 0 deletions homeassistant/components/litterrobot/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Support for Litter-Robot switches."""
from homeassistant.helpers.entity import ToggleEntity

from .const import DOMAIN
from .hub import LitterRobotEntity


class LitterRobotNightLightModeSwitch(LitterRobotEntity, ToggleEntity):
Comment thread
natekspencer marked this conversation as resolved.
"""Litter-Robot Night Light Mode Switch."""

@property
def is_on(self):
"""Return true if switch is on."""
return self.robot.night_light_active

@property
def icon(self):
"""Return the icon."""
return "mdi:lightbulb-on" if self.is_on else "mdi:lightbulb-off"

async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
await self.perform_action_and_refresh(self.robot.set_night_light, True)

async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
await self.perform_action_and_refresh(self.robot.set_night_light, False)


class LitterRobotPanelLockoutSwitch(LitterRobotEntity, ToggleEntity):
"""Litter-Robot Panel Lockout Switch."""

@property
def is_on(self):
"""Return true if switch is on."""
return self.robot.panel_lock_active

@property
def icon(self):
"""Return the icon."""
return "mdi:lock" if self.is_on else "mdi:lock-open"

async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
await self.perform_action_and_refresh(self.robot.set_panel_lockout, True)

async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
await self.perform_action_and_refresh(self.robot.set_panel_lockout, False)


ROBOT_SWITCHES = {
"Night Light Mode": LitterRobotNightLightModeSwitch,
"Panel Lockout": LitterRobotPanelLockoutSwitch,
}


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Litter-Robot switches using config entry."""
hub = hass.data[DOMAIN][config_entry.entry_id]

entities = []
for robot in hub.account.robots:
for switch_type, switch_class in ROBOT_SWITCHES.items():
entities.append(switch_class(robot, switch_type, hub))

if entities:
async_add_entities(entities, True)
13 changes: 13 additions & 0 deletions homeassistant/components/litterrobot/vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
VacuumEntity,
)
from homeassistant.const import STATE_OFF
import homeassistant.util.dt as dt_util

from .const import DOMAIN
from .hub import LitterRobotEntity
Expand Down Expand Up @@ -118,9 +119,21 @@ async def async_send_command(self, command, params=None, **kwargs):
@property
def device_state_attributes(self):
"""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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Times in state attributes must be absolute UTC time.

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 {
"clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes,
"is_sleeping": self.robot.is_sleeping,
"sleep_mode_start_time": sleep_mode_start_time,
"sleep_mode_end_time": sleep_mode_end_time,
"power_status": self.robot.power_status,
"unit_status_code": self.robot.unit_status.name,
"last_seen": self.robot.last_seen,
Expand Down
22 changes: 20 additions & 2 deletions tests/components/litterrobot/conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Configure pytest for Litter-Robot tests."""
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, patch

from pylitterbot import Robot
import pytest

from homeassistant.components import litterrobot
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .common import ROBOT_DATA
from .common import CONFIG, ROBOT_DATA

from tests.common import MockConfigEntry


def create_mock_robot(hass):
Expand All @@ -17,6 +19,8 @@ def create_mock_robot(hass):
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


Expand All @@ -33,3 +37,17 @@ def mock_hub(hass):
hub.coordinator.last_update_success = True
hub.account.robots = [create_mock_robot(hass)]
return hub


async def setup_hub(hass, mock_hub, platform_domain):
"""Load a Litter-Robot platform with the provided hub."""
entry = MockConfigEntry(
domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN],
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)

with patch.dict(hass.data, {litterrobot.DOMAIN: {entry.entry_id: mock_hub}}):
Comment thread
natekspencer marked this conversation as resolved.
Outdated
await hass.config_entries.async_forward_entry_setup(entry, platform_domain)
Comment thread
natekspencer marked this conversation as resolved.
await hass.async_block_till_done()
20 changes: 20 additions & 0 deletions tests/components/litterrobot/test_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Test the Litter-Robot sensor entity."""
from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN
from homeassistant.const import PERCENTAGE

from .conftest import setup_hub

ENTITY_ID = "sensor.test_waste_drawer"


async def test_sensor(hass, mock_hub):
"""Tests the sensor entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)

sensor = hass.states.get(ENTITY_ID)
assert sensor
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
59 changes: 59 additions & 0 deletions tests/components/litterrobot/test_switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Test the Litter-Robot switch entity."""
from datetime import timedelta

import pytest

from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME
from homeassistant.components.switch import (
DOMAIN as PLATFORM_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
from homeassistant.util.dt import utcnow

from .conftest import setup_hub

from tests.common import async_fire_time_changed

NIGHT_LIGHT_MODE_ENTITY_ID = "switch.test_night_light_mode"
PANEL_LOCKOUT_ENTITY_ID = "switch.test_panel_lockout"


async def test_switch(hass, mock_hub):
"""Tests the switch entity was set up."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)

switch = hass.states.get(NIGHT_LIGHT_MODE_ENTITY_ID)
assert switch
assert switch.state == STATE_ON


@pytest.mark.parametrize(
"entity_id,robot_command",
[
(NIGHT_LIGHT_MODE_ENTITY_ID, "set_night_light"),
(PANEL_LOCKOUT_ENTITY_ID, "set_panel_lockout"),
],
)
async def test_on_off_commands(hass, mock_hub, entity_id, robot_command):
"""Test sending commands to the switch."""
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)

switch = hass.states.get(entity_id)
assert switch

data = {ATTR_ENTITY_ID: entity_id}

count = 0
for service in [SERVICE_TURN_ON, SERVICE_TURN_OFF]:
count += 1
await hass.services.async_call(
PLATFORM_DOMAIN,
service,
data,
blocking=True,
)
future = utcnow() + timedelta(seconds=REFRESH_WAIT_TIME)
async_fire_time_changed(hass, future)
assert getattr(mock_hub.account.robots[0], robot_command).call_count == count
25 changes: 5 additions & 20 deletions tests/components/litterrobot/test_vacuum.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"""Test the Litter-Robot vacuum entity."""
from datetime import timedelta
from unittest.mock import patch

import pytest

from homeassistant.components import litterrobot
from homeassistant.components.litterrobot.hub import REFRESH_WAIT_TIME
from homeassistant.components.vacuum import (
ATTR_PARAMS,
Expand All @@ -18,32 +16,19 @@
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
from homeassistant.util.dt import utcnow

from .common import CONFIG
from .conftest import setup_hub

from tests.common import MockConfigEntry, async_fire_time_changed
from tests.common import async_fire_time_changed

ENTITY_ID = "vacuum.test_litter_box"


async def setup_hub(hass, mock_hub):
"""Load the Litter-Robot vacuum platform with the provided hub."""
hass.config.components.add(litterrobot.DOMAIN)
entry = MockConfigEntry(
domain=litterrobot.DOMAIN,
data=CONFIG[litterrobot.DOMAIN],
)

with patch.dict(hass.data, {litterrobot.DOMAIN: {entry.entry_id: mock_hub}}):
await hass.config_entries.async_forward_entry_setup(entry, PLATFORM_DOMAIN)
await hass.async_block_till_done()


async def test_vacuum(hass, mock_hub):
"""Tests the vacuum entity was set up."""
await setup_hub(hass, mock_hub)
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)

vacuum = hass.states.get(ENTITY_ID)
assert vacuum is not None
assert vacuum
assert vacuum.state == STATE_DOCKED
assert vacuum.attributes["is_sleeping"] is False

Expand Down Expand Up @@ -71,7 +56,7 @@ async def test_vacuum(hass, mock_hub):
)
async def test_commands(hass, mock_hub, service, command, extra):
"""Test sending commands to the vacuum."""
await setup_hub(hass, mock_hub)
await setup_hub(hass, mock_hub, PLATFORM_DOMAIN)

vacuum = hass.states.get(ENTITY_ID)
assert vacuum is not None
Expand Down