diff --git a/.coveragerc b/.coveragerc index e11a268217b..70a74e0a356 100644 --- a/.coveragerc +++ b/.coveragerc @@ -865,12 +865,8 @@ omit = homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py homeassistant/components/rituals_perfume_genie/binary_sensor.py - homeassistant/components/rituals_perfume_genie/entity.py homeassistant/components/rituals_perfume_genie/number.py homeassistant/components/rituals_perfume_genie/select.py - homeassistant/components/rituals_perfume_genie/sensor.py - homeassistant/components/rituals_perfume_genie/switch.py - homeassistant/components/rituals_perfume_genie/__init__.py homeassistant/components/rocketchat/notify.py homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py diff --git a/tests/components/rituals_perfume_genie/common.py b/tests/components/rituals_perfume_genie/common.py new file mode 100644 index 00000000000..35555e2b842 --- /dev/null +++ b/tests/components/rituals_perfume_genie/common.py @@ -0,0 +1,94 @@ +"""Common methods used across tests for Rituals Perfume Genie.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant.components.rituals_perfume_genie.const import ACCOUNT_HASH, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +def mock_config_entry(uniqe_id: str, entry_id: str = "an_entry_id") -> MockConfigEntry: + """Return a mock Config Entry for the Rituals Perfume Genie integration.""" + return MockConfigEntry( + domain=DOMAIN, + title="name@example.com", + unique_id=uniqe_id, + data={ACCOUNT_HASH: "an_account_hash"}, + entry_id=entry_id, + ) + + +def mock_diffuser( + hublot: str, + available: bool = True, + battery_percentage: int | Exception = 100, + charging: bool | Exception = True, + fill: str = "90-100%", + has_battery: bool = True, + has_cartridge: bool = True, + is_on: bool = True, + name: str = "Genie", + perfume: str = "Ritual of Sakura", + version: str = "4.0", + wifi_percentage: int = 75, +) -> MagicMock: + """Return a mock Diffuser initialized with the given data.""" + diffuser_mock = MagicMock() + diffuser_mock.available = available + diffuser_mock.battery_percentage = battery_percentage + diffuser_mock.charging = charging + diffuser_mock.fill = fill + diffuser_mock.has_battery = has_battery + diffuser_mock.has_cartridge = has_cartridge + diffuser_mock.hublot = hublot + diffuser_mock.is_on = is_on + diffuser_mock.name = name + diffuser_mock.perfume = perfume + diffuser_mock.turn_off = AsyncMock() + diffuser_mock.turn_on = AsyncMock() + diffuser_mock.update_data = AsyncMock() + diffuser_mock.version = version + diffuser_mock.wifi_percentage = wifi_percentage + return diffuser_mock + + +def mock_diffuser_v1_battery_cartridge(): + """Create and return a mock version 1 Diffuser with battery and a cartridge.""" + return mock_diffuser(hublot="lot123v1") + + +def mock_diffuser_v2_no_battery_no_cartridge(): + """Create and return a mock version 2 Diffuser without battery and cartridge.""" + return mock_diffuser( + hublot="lot123v2", + battery_percentage=Exception(), + charging=Exception(), + has_battery=False, + has_cartridge=False, + name="Genie V2", + perfume="No Cartridge", + version="5.0", + ) + + +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_diffusers: list[MagicMock] = [mock_diffuser(hublot="lot123")], +) -> None: + """Initialize the Rituals Perfume Genie integration with the given Config Entry and Diffuser list.""" + mock_config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.rituals_perfume_genie.Account.get_devices", + return_value=mock_diffusers, + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_config_entry.entry_id in hass.data[DOMAIN] + assert hass.data[DOMAIN] + + await hass.async_block_till_done() diff --git a/tests/components/rituals_perfume_genie/test_init.py b/tests/components/rituals_perfume_genie/test_init.py new file mode 100644 index 00000000000..887417a41f8 --- /dev/null +++ b/tests/components/rituals_perfume_genie/test_init.py @@ -0,0 +1,34 @@ +"""Tests for the Rituals Perfume Genie integration.""" +from unittest.mock import patch + +import aiohttp + +from homeassistant.components.rituals_perfume_genie.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .common import init_integration, mock_config_entry + + +async def test_config_entry_not_ready(hass: HomeAssistant): + """Test the Rituals configuration entry setup if connection to Rituals is missing.""" + config_entry = mock_config_entry(uniqe_id="id_123_not_ready") + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.rituals_perfume_genie.Account.get_devices", + side_effect=aiohttp.ClientError, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_unload(hass: HomeAssistant) -> None: + """Test the Rituals Perfume Genie configuration entry setup and unloading.""" + config_entry = mock_config_entry(uniqe_id="id_123_unload") + await init_integration(hass, config_entry) + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.entry_id not in hass.data[DOMAIN] diff --git a/tests/components/rituals_perfume_genie/test_sensor.py b/tests/components/rituals_perfume_genie/test_sensor.py new file mode 100644 index 00000000000..477353d3b83 --- /dev/null +++ b/tests/components/rituals_perfume_genie/test_sensor.py @@ -0,0 +1,88 @@ +"""Tests for the Rituals Perfume Genie sensor platform.""" +from homeassistant.components.rituals_perfume_genie.sensor import ( + BATTERY_SUFFIX, + FILL_SUFFIX, + PERFUME_SUFFIX, + WIFI_SUFFIX, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + PERCENTAGE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry + +from .common import ( + init_integration, + mock_config_entry, + mock_diffuser_v1_battery_cartridge, + mock_diffuser_v2_no_battery_no_cartridge, +) + + +async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> None: + """Test the creation and values of the Rituals Perfume Genie sensors.""" + config_entry = mock_config_entry(uniqe_id="id_123_sensor_test_diffuser_v1") + diffuser = mock_diffuser_v1_battery_cartridge() + await init_integration(hass, config_entry, [diffuser]) + registry = entity_registry.async_get(hass) + hublot = diffuser.hublot + + state = hass.states.get("sensor.genie_perfume") + assert state + assert state.state == diffuser.perfume + assert state.attributes.get(ATTR_ICON) == "mdi:tag-text" + + entry = registry.async_get("sensor.genie_perfume") + assert entry + assert entry.unique_id == f"{hublot}{PERFUME_SUFFIX}" + + state = hass.states.get("sensor.genie_fill") + assert state + assert state.state == diffuser.fill + assert state.attributes.get(ATTR_ICON) == "mdi:beaker" + + entry = registry.async_get("sensor.genie_fill") + assert entry + assert entry.unique_id == f"{hublot}{FILL_SUFFIX}" + + state = hass.states.get("sensor.genie_battery") + assert state + assert state.state == str(diffuser.battery_percentage) + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_BATTERY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.genie_battery") + assert entry + assert entry.unique_id == f"{hublot}{BATTERY_SUFFIX}" + + state = hass.states.get("sensor.genie_wifi") + assert state + assert state.state == str(diffuser.wifi_percentage) + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.genie_wifi") + assert entry + assert entry.unique_id == f"{hublot}{WIFI_SUFFIX}" + + +async def test_sensors_diffuser_v2_no_battery_no_cartridge(hass: HomeAssistant) -> None: + """Test the creation and values of the Rituals Perfume Genie sensors.""" + config_entry = mock_config_entry(uniqe_id="id_123_sensor_test_diffuser_v2") + + await init_integration( + hass, config_entry, [mock_diffuser_v2_no_battery_no_cartridge()] + ) + + state = hass.states.get("sensor.genie_v2_perfume") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:tag-remove" + + state = hass.states.get("sensor.genie_v2_fill") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:beaker-question" diff --git a/tests/components/rituals_perfume_genie/test_switch.py b/tests/components/rituals_perfume_genie/test_switch.py new file mode 100644 index 00000000000..a2691da0e0e --- /dev/null +++ b/tests/components/rituals_perfume_genie/test_switch.py @@ -0,0 +1,104 @@ +"""Tests for the Rituals Perfume Genie switch platform.""" +from __future__ import annotations + +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY +from homeassistant.components.rituals_perfume_genie.const import COORDINATORS, DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component + +from .common import ( + init_integration, + mock_config_entry, + mock_diffuser_v1_battery_cartridge, +) + + +async def test_switch_entity(hass: HomeAssistant) -> None: + """Test the creation and values of the Rituals Perfume Genie diffuser switch.""" + config_entry = mock_config_entry(uniqe_id="id_123_switch_set_state_test") + diffuser = mock_diffuser_v1_battery_cartridge() + await init_integration(hass, config_entry, [diffuser]) + + registry = entity_registry.async_get(hass) + + state = hass.states.get("switch.genie") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ICON) == "mdi:fan" + + entry = registry.async_get("switch.genie") + assert entry + assert entry.unique_id == diffuser.hublot + + +async def test_switch_handle_coordinator_update(hass: HomeAssistant) -> None: + """Test handling a coordinator update.""" + config_entry = mock_config_entry(uniqe_id="id_123_switch_set_state_test") + diffuser = mock_diffuser_v1_battery_cartridge() + await init_integration(hass, config_entry, [diffuser]) + await async_setup_component(hass, "homeassistant", {}) + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS]["lot123v1"] + diffuser.is_on = False + + state = hass.states.get("switch.genie") + assert state + assert state.state == STATE_ON + + call_count_before_update = diffuser.update_data.call_count + + await hass.services.async_call( + "homeassistant", + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["switch.genie"]}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.genie") + assert state + assert state.state == STATE_OFF + + assert coordinator.last_update_success + assert diffuser.update_data.call_count == call_count_before_update + 1 + + +async def test_set_switch_state(hass: HomeAssistant) -> None: + """Test changing the diffuser switch entity state.""" + config_entry = mock_config_entry(uniqe_id="id_123_switch_set_state_test") + await init_integration(hass, config_entry, [mock_diffuser_v1_battery_cartridge()]) + + state = hass.states.get("switch.genie") + assert state + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.genie"}, + blocking=True, + ) + + state = hass.states.get("switch.genie") + assert state + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.genie"}, + blocking=True, + ) + + state = hass.states.get("switch.genie") + assert state + assert state.state == STATE_ON