Add Shelly motion sensor switch (#115312)
* Add Shelly motion sensor switch * update name * make motion switch a restore entity * add test * apply review comment * Update tests/components/shelly/test_switch.py Co-authored-by: Shay Levy <levyshay1@gmail.com> * Update tests/components/shelly/test_switch.py Co-authored-by: Shay Levy <levyshay1@gmail.com> * Update tests/components/shelly/test_switch.py Co-authored-by: Shay Levy <levyshay1@gmail.com> * Update tests/components/shelly/test_switch.py Co-authored-by: Shay Levy <levyshay1@gmail.com> * rename switch * Update tests/components/shelly/test_switch.py Co-authored-by: Shay Levy <levyshay1@gmail.com> * Update tests/components/shelly/test_switch.py Co-authored-by: Shay Levy <levyshay1@gmail.com> * Update tests/components/shelly/test_switch.py Co-authored-by: Shay Levy <levyshay1@gmail.com> * fix ruff --------- Co-authored-by: Shay Levy <levyshay1@gmail.com>pull/117558/head
parent
53da59a454
commit
32a9cb4b14
|
@ -72,6 +72,7 @@ BLOCK_SLEEPING_PLATFORMS: Final = [
|
|||
Platform.CLIMATE,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
RPC_PLATFORMS: Final = [
|
||||
Platform.BINARY_SENSOR,
|
||||
|
|
|
@ -45,6 +45,11 @@ RGBW_MODELS: Final = (
|
|||
MODEL_RGBW2,
|
||||
)
|
||||
|
||||
MOTION_MODELS: Final = (
|
||||
MODEL_MOTION,
|
||||
MODEL_MOTION_2,
|
||||
)
|
||||
|
||||
MODELS_SUPPORTING_LIGHT_TRANSITION: Final = (
|
||||
MODEL_DUO,
|
||||
MODEL_BULB_RGBW,
|
||||
|
|
|
@ -22,18 +22,23 @@ from homeassistant.components.switch import (
|
|||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import STATE_ON, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import DOMAIN, GAS_VALVE_OPEN_STATES
|
||||
from .const import CONF_SLEEP_PERIOD, DOMAIN, GAS_VALVE_OPEN_STATES, MOTION_MODELS
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
ShellyBlockAttributeEntity,
|
||||
ShellyBlockEntity,
|
||||
ShellyRpcEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
async_setup_block_attribute_entities,
|
||||
async_setup_entry_attribute_entities,
|
||||
)
|
||||
from .utils import (
|
||||
async_remove_shelly_entity,
|
||||
|
@ -60,6 +65,12 @@ GAS_VALVE_SWITCH = BlockSwitchDescription(
|
|||
entity_registry_enabled_default=False,
|
||||
)
|
||||
|
||||
MOTION_SWITCH = BlockSwitchDescription(
|
||||
key="sensor|motionActive",
|
||||
name="Motion detection",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -94,6 +105,20 @@ def async_setup_block_entry(
|
|||
)
|
||||
return
|
||||
|
||||
# Add Shelly Motion as a switch
|
||||
if coordinator.model in MOTION_MODELS:
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
config_entry,
|
||||
async_add_entities,
|
||||
{("sensor", "motionActive"): MOTION_SWITCH},
|
||||
BlockSleepingMotionSwitch,
|
||||
)
|
||||
return
|
||||
|
||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||
return
|
||||
|
||||
# In roller mode the relay blocks exist but do not contain required info
|
||||
if (
|
||||
coordinator.model in [MODEL_2, MODEL_25]
|
||||
|
@ -165,6 +190,54 @@ def async_setup_rpc_entry(
|
|||
async_add_entities(RpcRelaySwitch(coordinator, id_) for id_ in switch_ids)
|
||||
|
||||
|
||||
class BlockSleepingMotionSwitch(
|
||||
ShellySleepingBlockAttributeEntity, RestoreEntity, SwitchEntity
|
||||
):
|
||||
"""Entity that controls Motion Sensor on Block based Shelly devices."""
|
||||
|
||||
entity_description: BlockSwitchDescription
|
||||
_attr_translation_key = "motion_switch"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyBlockCoordinator,
|
||||
block: Block | None,
|
||||
attribute: str,
|
||||
description: BlockSwitchDescription,
|
||||
entry: RegistryEntry | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sleeping sensor."""
|
||||
super().__init__(coordinator, block, attribute, description, entry)
|
||||
self.last_state: State | None = None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""If motion is active."""
|
||||
if self.block is not None:
|
||||
return bool(self.block.motionActive)
|
||||
|
||||
if self.last_state is None:
|
||||
return None
|
||||
|
||||
return self.last_state.state == STATE_ON
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Activate switch."""
|
||||
await self.coordinator.device.set_shelly_motion_detection(True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Deactivate switch."""
|
||||
await self.coordinator.device.set_shelly_motion_detection(False)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
if (last_state := await self.async_get_last_state()) is not None:
|
||||
self.last_state = last_state
|
||||
|
||||
|
||||
class BlockValveSwitch(ShellyBlockAttributeEntity, SwitchEntity):
|
||||
"""Entity that controls a Gas Valve on Block based Shelly devices.
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ MOCK_BLOCKS = [
|
|||
set_state=AsyncMock(side_effect=mock_light_set_state),
|
||||
),
|
||||
Mock(
|
||||
sensor_ids={"motion": 0, "temp": 22.1, "gas": "mild"},
|
||||
sensor_ids={"motion": 0, "temp": 22.1, "gas": "mild", "motionActive": 1},
|
||||
channel="0",
|
||||
motion=0,
|
||||
temp=22.1,
|
||||
|
|
|
@ -11,7 +11,11 @@ from homeassistant.components import automation, script
|
|||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.components.shelly.const import DOMAIN, MODEL_WALL_DISPLAY
|
||||
from homeassistant.components.shelly.const import (
|
||||
DOMAIN,
|
||||
MODEL_WALL_DISPLAY,
|
||||
MOTION_MODELS,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
|
@ -20,17 +24,22 @@ from homeassistant.const import (
|
|||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
import homeassistant.helpers.issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import init_integration, register_entity
|
||||
from . import get_entity_state, init_integration, register_device, register_entity
|
||||
|
||||
from tests.common import mock_restore_cache
|
||||
|
||||
RELAY_BLOCK_ID = 0
|
||||
GAS_VALVE_BLOCK_ID = 6
|
||||
MOTION_BLOCK_ID = 3
|
||||
|
||||
|
||||
async def test_block_device_services(
|
||||
|
@ -56,6 +65,121 @@ async def test_block_device_services(
|
|||
assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MOTION_MODELS)
|
||||
async def test_block_motion_switch(
|
||||
hass: HomeAssistant,
|
||||
model: str,
|
||||
mock_block_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test Shelly motion active turn on/off services."""
|
||||
entity_id = "switch.test_name_motion_detection"
|
||||
await init_integration(hass, 1, sleep_period=1000, model=model)
|
||||
|
||||
# Make device online
|
||||
mock_block_device.mock_online()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert get_entity_state(hass, entity_id) == STATE_ON
|
||||
|
||||
# turn off
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
monkeypatch.setattr(mock_block_device.blocks[MOTION_BLOCK_ID], "motionActive", 0)
|
||||
mock_block_device.mock_update()
|
||||
|
||||
mock_block_device.set_shelly_motion_detection.assert_called_once_with(False)
|
||||
assert get_entity_state(hass, entity_id) == STATE_OFF
|
||||
|
||||
# turn on
|
||||
mock_block_device.set_shelly_motion_detection.reset_mock()
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
monkeypatch.setattr(mock_block_device.blocks[MOTION_BLOCK_ID], "motionActive", 1)
|
||||
mock_block_device.mock_update()
|
||||
|
||||
mock_block_device.set_shelly_motion_detection.assert_called_once_with(True)
|
||||
assert get_entity_state(hass, entity_id) == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MOTION_MODELS)
|
||||
async def test_block_restored_motion_switch(
|
||||
hass: HomeAssistant,
|
||||
model: str,
|
||||
mock_block_device: Mock,
|
||||
device_reg: DeviceRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test block restored motion active switch."""
|
||||
entry = await init_integration(
|
||||
hass, 1, sleep_period=1000, model=model, skip_setup=True
|
||||
)
|
||||
register_device(device_reg, entry)
|
||||
entity_id = register_entity(
|
||||
hass,
|
||||
SWITCH_DOMAIN,
|
||||
"test_name_motion_detection",
|
||||
"sensor_0-motionActive",
|
||||
entry,
|
||||
)
|
||||
|
||||
mock_restore_cache(hass, [State(entity_id, STATE_OFF)])
|
||||
monkeypatch.setattr(mock_block_device, "initialized", False)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert get_entity_state(hass, entity_id) == STATE_OFF
|
||||
|
||||
# Make device online
|
||||
monkeypatch.setattr(mock_block_device, "initialized", True)
|
||||
mock_block_device.mock_online()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert get_entity_state(hass, entity_id) == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", MOTION_MODELS)
|
||||
async def test_block_restored_motion_switch_no_last_state(
|
||||
hass: HomeAssistant,
|
||||
model: str,
|
||||
mock_block_device: Mock,
|
||||
device_reg: DeviceRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test block restored motion active switch missing last state."""
|
||||
entry = await init_integration(
|
||||
hass, 1, sleep_period=1000, model=model, skip_setup=True
|
||||
)
|
||||
register_device(device_reg, entry)
|
||||
entity_id = register_entity(
|
||||
hass,
|
||||
SWITCH_DOMAIN,
|
||||
"test_name_motion_detection",
|
||||
"sensor_0-motionActive",
|
||||
entry,
|
||||
)
|
||||
monkeypatch.setattr(mock_block_device, "initialized", False)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert get_entity_state(hass, entity_id) == STATE_UNKNOWN
|
||||
|
||||
# Make device online
|
||||
monkeypatch.setattr(mock_block_device, "initialized", True)
|
||||
mock_block_device.mock_online()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert get_entity_state(hass, entity_id) == STATE_ON
|
||||
|
||||
|
||||
async def test_block_device_unique_ids(
|
||||
hass: HomeAssistant, entity_registry: EntityRegistry, mock_block_device: Mock
|
||||
) -> None:
|
||||
|
|
Loading…
Reference in New Issue