mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
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>
This commit is contained in:
parent
53da59a454
commit
32a9cb4b14
@ -72,6 +72,7 @@ BLOCK_SLEEPING_PLATFORMS: Final = [
|
|||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.NUMBER,
|
Platform.NUMBER,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
RPC_PLATFORMS: Final = [
|
RPC_PLATFORMS: Final = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
|
@ -45,6 +45,11 @@ RGBW_MODELS: Final = (
|
|||||||
MODEL_RGBW2,
|
MODEL_RGBW2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MOTION_MODELS: Final = (
|
||||||
|
MODEL_MOTION,
|
||||||
|
MODEL_MOTION_2,
|
||||||
|
)
|
||||||
|
|
||||||
MODELS_SUPPORTING_LIGHT_TRANSITION: Final = (
|
MODELS_SUPPORTING_LIGHT_TRANSITION: Final = (
|
||||||
MODEL_DUO,
|
MODEL_DUO,
|
||||||
MODEL_BULB_RGBW,
|
MODEL_BULB_RGBW,
|
||||||
|
@ -22,18 +22,23 @@ from homeassistant.components.switch import (
|
|||||||
SwitchEntityDescription,
|
SwitchEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN
|
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_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
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 .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||||
from .entity import (
|
from .entity import (
|
||||||
BlockEntityDescription,
|
BlockEntityDescription,
|
||||||
ShellyBlockAttributeEntity,
|
ShellyBlockAttributeEntity,
|
||||||
ShellyBlockEntity,
|
ShellyBlockEntity,
|
||||||
ShellyRpcEntity,
|
ShellyRpcEntity,
|
||||||
|
ShellySleepingBlockAttributeEntity,
|
||||||
async_setup_block_attribute_entities,
|
async_setup_block_attribute_entities,
|
||||||
|
async_setup_entry_attribute_entities,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
async_remove_shelly_entity,
|
async_remove_shelly_entity,
|
||||||
@ -60,6 +65,12 @@ GAS_VALVE_SWITCH = BlockSwitchDescription(
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MOTION_SWITCH = BlockSwitchDescription(
|
||||||
|
key="sensor|motionActive",
|
||||||
|
name="Motion detection",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -94,6 +105,20 @@ def async_setup_block_entry(
|
|||||||
)
|
)
|
||||||
return
|
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
|
# In roller mode the relay blocks exist but do not contain required info
|
||||||
if (
|
if (
|
||||||
coordinator.model in [MODEL_2, MODEL_25]
|
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)
|
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):
|
class BlockValveSwitch(ShellyBlockAttributeEntity, SwitchEntity):
|
||||||
"""Entity that controls a Gas Valve on Block based Shelly devices.
|
"""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),
|
set_state=AsyncMock(side_effect=mock_light_set_state),
|
||||||
),
|
),
|
||||||
Mock(
|
Mock(
|
||||||
sensor_ids={"motion": 0, "temp": 22.1, "gas": "mild"},
|
sensor_ids={"motion": 0, "temp": 22.1, "gas": "mild", "motionActive": 1},
|
||||||
channel="0",
|
channel="0",
|
||||||
motion=0,
|
motion=0,
|
||||||
temp=22.1,
|
temp=22.1,
|
||||||
|
@ -11,7 +11,11 @@ from homeassistant.components import automation, script
|
|||||||
from homeassistant.components.automation import automations_with_entity
|
from homeassistant.components.automation import automations_with_entity
|
||||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||||
from homeassistant.components.script import scripts_with_entity
|
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.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -20,17 +24,22 @@ from homeassistant.const import (
|
|||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, State
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
import homeassistant.helpers.issue_registry as ir
|
import homeassistant.helpers.issue_registry as ir
|
||||||
from homeassistant.setup import async_setup_component
|
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
|
RELAY_BLOCK_ID = 0
|
||||||
GAS_VALVE_BLOCK_ID = 6
|
GAS_VALVE_BLOCK_ID = 6
|
||||||
|
MOTION_BLOCK_ID = 3
|
||||||
|
|
||||||
|
|
||||||
async def test_block_device_services(
|
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
|
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(
|
async def test_block_device_unique_ids(
|
||||||
hass: HomeAssistant, entity_registry: EntityRegistry, mock_block_device: Mock
|
hass: HomeAssistant, entity_registry: EntityRegistry, mock_block_device: Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user