diff --git a/.coveragerc b/.coveragerc index e75460f742f..0bfd1fbbbeb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1109,7 +1109,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/climate.py homeassistant/components/shelly/coordinator.py homeassistant/components/shelly/entity.py diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index cfacdf85cfd..a5265241da3 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -268,10 +268,8 @@ class RestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity): entity_description: RestBinarySensorDescription @property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return true if REST sensor state is on.""" - if self.attribute_value is None: - return None return bool(self.attribute_value) @@ -281,10 +279,8 @@ class RpcBinarySensor(ShellyRpcAttributeEntity, BinarySensorEntity): entity_description: RpcBinarySensorDescription @property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return true if RPC sensor state is on.""" - if self.attribute_value is None: - return None return bool(self.attribute_value) @@ -308,7 +304,7 @@ class RpcSleepingBinarySensor(ShellySleepingRpcAttributeEntity, BinarySensorEnti entity_description: RpcBinarySensorDescription @property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return true if RPC sensor state is on.""" if self.coordinator.device.initialized: return bool(self.attribute_value) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index a844367bced..36165afe72d 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -2,16 +2,25 @@ from __future__ import annotations from copy import deepcopy +from datetime import timedelta from typing import Any from unittest.mock import Mock import pytest -from homeassistant.components.shelly.const import CONF_SLEEP_PERIOD, DOMAIN +from homeassistant.components.shelly.const import ( + CONF_SLEEP_PERIOD, + DOMAIN, + REST_SENSORS_UPDATE_INTERVAL, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed MOCK_MAC = "123456789ABC" @@ -22,6 +31,7 @@ async def init_integration( model="SHSW-25", sleep_period=0, options: dict[str, Any] | None = None, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the Shelly integration in Home Assistant.""" data = { @@ -36,8 +46,9 @@ async def init_integration( ) entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry @@ -63,3 +74,44 @@ def inject_rpc_device_event( """Inject event for rpc device.""" monkeypatch.setattr(mock_rpc_device, "event", event) mock_rpc_device.mock_event() + + +async def mock_rest_update(hass: HomeAssistant): + """Move time to create REST sensors update event.""" + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) + ) + await hass.async_block_till_done() + + +def register_entity( + hass: HomeAssistant, + domain: str, + object_id: str, + unique_id: str, + config_entry: ConfigEntry | None = None, +) -> str: + """Register enabled entity, return entity_id.""" + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + domain, + DOMAIN, + f"{MOCK_MAC}-{unique_id}", + suggested_object_id=object_id, + disabled_by=None, + config_entry=config_entry, + ) + return f"{domain}.{object_id}" + + +def register_device(device_reg, config_entry: ConfigEntry): + """Register Shelly device.""" + device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={ + ( + device_registry.CONNECTION_NETWORK_MAC, + device_registry.format_mac(MOCK_MAC), + ) + }, + ) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index f27f91cbfe7..5e1655c7d46 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -63,9 +63,11 @@ def mock_light_set_state( MOCK_BLOCKS = [ Mock( - sensor_ids={"inputEvent": "S", "inputEventCnt": 2}, + sensor_ids={"inputEvent": "S", "inputEventCnt": 2, "overpower": 0}, channel="0", type="relay", + overpower=0, + description="relay_0", set_state=AsyncMock(side_effect=lambda turn: {"ison": turn == "on"}), ), Mock( @@ -88,6 +90,12 @@ MOCK_BLOCKS = [ type="light", set_state=AsyncMock(side_effect=mock_light_set_state), ), + Mock( + sensor_ids={"motion": 0}, + motion=0, + description="sensor_0", + type="sensor", + ), ] MOCK_CONFIG = { @@ -135,7 +143,13 @@ MOCK_STATUS_COAP = { MOCK_STATUS_RPC = { "switch:0": {"output": True}, - "cover:0": {"state": "stopped", "pos_control": True, "current_pos": 50}, + "cloud": {"connected": False}, + "cover:0": { + "state": "stopped", + "pos_control": True, + "current_pos": 50, + "apower": 85.3, + }, "sys": { "available_updates": { "beta": {"version": "some_beta_version"}, diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py new file mode 100644 index 00000000000..c7e2faaf47c --- /dev/null +++ b/tests/components/shelly/test_binary_sensor.py @@ -0,0 +1,155 @@ +"""Tests for Shelly binary sensor platform.""" + + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import State + +from . import ( + init_integration, + mock_rest_update, + mutate_rpc_device_status, + register_device, + register_entity, +) + +from tests.common import mock_restore_cache + +RELAY_BLOCK_ID = 0 +SENSOR_BLOCK_ID = 3 + + +async def test_block_binary_sensor(hass, mock_block_device, monkeypatch): + """Test block binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_channel_1_overpowering" + await init_integration(hass, 1) + + assert hass.states.get(entity_id).state == STATE_OFF + + monkeypatch.setattr(mock_block_device.blocks[RELAY_BLOCK_ID], "overpower", 1) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_block_rest_binary_sensor(hass, mock_block_device, monkeypatch): + """Test block REST binary sensor.""" + entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") + monkeypatch.setitem(mock_block_device.status, "cloud", {"connected": False}) + await init_integration(hass, 1) + + assert hass.states.get(entity_id).state == STATE_OFF + + monkeypatch.setitem(mock_block_device.status["cloud"], "connected", True) + await mock_rest_update(hass) + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_block_sleeping_binary_sensor(hass, mock_block_device, monkeypatch): + """Test block sleeping binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_motion" + await init_integration(hass, 1, sleep_period=1000) + + # Sensor should be created when device is online + assert hass.states.get(entity_id) is None + + # Make device online + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "motion", 1) + mock_block_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_block_restored_sleeping_binary_sensor( + hass, mock_block_device, device_reg, monkeypatch +): + """Test block restored sleeping binary sensor.""" + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_ON + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + +async def test_rpc_binary_sensor(hass, mock_rpc_device, monkeypatch) -> None: + """Test RPC binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" + await init_integration(hass, 2) + + assert hass.states.get(entity_id).state == STATE_OFF + + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "cover:0", "errors", "overpower" + ) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_rpc_sleeping_binary_sensor( + hass, mock_rpc_device, device_reg, monkeypatch +) -> None: + """Test RPC online sleeping binary sensor.""" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_cloud" + entry = await init_integration(hass, 2, sleep_period=1000) + + # Sensor should be created when device is online + assert hass.states.get(entity_id) is None + + register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry) + + # Make device online + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cloud", "connected", True) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_rpc_restored_sleeping_binary_sensor( + hass, mock_rpc_device, device_reg, monkeypatch +): + """Test RPC restored binary sensor.""" + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + ) + + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_ON + + # Make device online + monkeypatch.setattr(mock_rpc_device, "initialized", True) + mock_rpc_device.mock_update() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 4da81e076ae..fe7d979e797 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -1,5 +1,4 @@ """Tests for Shelly update platform.""" -from datetime import timedelta from unittest.mock import AsyncMock from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError @@ -7,7 +6,7 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.shelly.const import DOMAIN, REST_SENSORS_UPDATE_INTERVAL +from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, @@ -20,11 +19,8 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import dt -from . import MOCK_MAC, init_integration - -from tests.common import async_fire_time_changed +from . import MOCK_MAC, init_integration, mock_rest_update @pytest.mark.parametrize( @@ -100,10 +96,7 @@ async def test_block_update(hass: HomeAssistant, mock_block_device, monkeypatch) assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_firmware_update") assert state.state == STATE_OFF @@ -134,10 +127,7 @@ async def test_block_beta_update(hass: HomeAssistant, mock_block_device, monkeyp assert state.attributes[ATTR_IN_PROGRESS] is False monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "2b") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_ON @@ -160,10 +150,7 @@ async def test_block_beta_update(hass: HomeAssistant, mock_block_device, monkeyp assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2b") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_OFF @@ -288,10 +275,7 @@ async def test_rpc_update(hass: HomeAssistant, mock_rpc_device, monkeypatch): assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_firmware_update") assert state.state == STATE_OFF @@ -335,10 +319,7 @@ async def test_rpc_beta_update(hass: HomeAssistant, mock_rpc_device, monkeypatch "beta": {"version": "2b"}, }, ) - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_ON @@ -361,10 +342,7 @@ async def test_rpc_beta_update(hass: HomeAssistant, mock_rpc_device, monkeypatch assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2b") - async_fire_time_changed( - hass, dt.utcnow() + timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL) - ) - await hass.async_block_till_done() + await mock_rest_update(hass) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_OFF