diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 7bec1ab1686..e1261411da3 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -73,6 +73,17 @@ class RpcBinarySensor(ShellyRpcAttributeEntity, BinarySensorEntity): return bool(self.attribute_value) +class RpcPresenceBinarySensor(RpcBinarySensor): + """Represent a RPC binary sensor entity for presence component.""" + + @property + def available(self) -> bool: + """Available.""" + available = super().available + + return available and self.coordinator.device.config[self.key]["enable"] + + class RpcBluTrvBinarySensor(RpcBinarySensor): """Represent a RPC BluTrv binary sensor.""" @@ -283,6 +294,14 @@ RPC_SENSORS: Final = { name="Mute", entity_category=EntityCategory.DIAGNOSTIC, ), + "presence_num_objects": RpcBinarySensorDescription( + key="presence", + sub_key="num_objects", + value=lambda status, _: bool(status), + name="Occupancy", + device_class=BinarySensorDeviceClass.OCCUPANCY, + entity_class=RpcPresenceBinarySensor, + ), } diff --git a/homeassistant/components/shelly/icons.json b/homeassistant/components/shelly/icons.json index 6760400a1f7..832cf2b4c8f 100644 --- a/homeassistant/components/shelly/icons.json +++ b/homeassistant/components/shelly/icons.json @@ -20,6 +20,9 @@ } }, "sensor": { + "detected_objects": { + "default": "mdi:account-group" + }, "gas_concentration": { "default": "mdi:gauge" }, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 2a1478f1307..a357ebdbd44 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -124,6 +124,17 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity): return self.option_map[attribute_value] +class RpcPresenceSensor(RpcSensor): + """Represent a RPC presence sensor.""" + + @property + def available(self) -> bool: + """Available.""" + available = super().available + + return available and self.coordinator.device.config[self.key]["enable"] + + class RpcEmeterPhaseSensor(RpcSensor): """Represent a RPC energy meter phase sensor.""" @@ -1428,6 +1439,14 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENUM, options=["dark", "twilight", "bright"], ), + "presence_num_objects": RpcSensorDescription( + key="presence", + sub_key="num_objects", + translation_key="detected_objects", + name="Detected objects", + state_class=SensorStateClass.MEASUREMENT, + entity_class=RpcPresenceSensor, + ), } diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 0c1d7051275..e8b789c5582 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -141,6 +141,9 @@ } }, "sensor": { + "detected_objects": { + "unit_of_measurement": "objects" + }, "gas_detected": { "state": { "none": "None", diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 061c22cf512..70e324b6c99 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -10,7 +10,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry @@ -527,3 +527,44 @@ async def test_rpc_flood_entities( entry = entity_registry.async_get(entity_id) assert entry == snapshot(name=f"{entity_id}-entry") + + +async def test_rpc_presence_component( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + entity_registry: EntityRegistry, +) -> None: + """Test RPC binary sensor entity for presence component.""" + config = deepcopy(mock_rpc_device.config) + config["presence"] = {"enable": True} + monkeypatch.setattr(mock_rpc_device, "config", config) + + status = deepcopy(mock_rpc_device.status) + status["presence"] = {"num_objects": 2} + monkeypatch.setattr(mock_rpc_device, "status", status) + + mock_config_entry = await init_integration(hass, 4) + + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_occupancy" + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_ON + + assert (entry := entity_registry.async_get(entity_id)) + assert entry.unique_id == "123456789ABC-presence-presence_num_objects" + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "presence", "num_objects", 0) + mock_rpc_device.mock_update() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_OFF + + config = deepcopy(mock_rpc_device.config) + config["presence"] = {"enable": False} + monkeypatch.setattr(mock_rpc_device, "config", config) + await hass.config_entries.async_reload(mock_config_entry.entry_id) + mock_rpc_device.mock_update() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index f2d86849854..6ab342b2cf8 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -1630,3 +1630,44 @@ async def test_block_friendly_name_sleeping_sensor( assert (state := hass.states.get(entity.entity_id)) assert state.attributes[ATTR_FRIENDLY_NAME] == "Test name Temperature" + + +async def test_rpc_presence_component( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, + entity_registry: EntityRegistry, +) -> None: + """Test RPC sensor entity for presence component.""" + config = deepcopy(mock_rpc_device.config) + config["presence"] = {"enable": True} + monkeypatch.setattr(mock_rpc_device, "config", config) + + status = deepcopy(mock_rpc_device.status) + status["presence"] = {"num_objects": 2} + monkeypatch.setattr(mock_rpc_device, "status", status) + + mock_config_entry = await init_integration(hass, 4) + + entity_id = f"{SENSOR_DOMAIN}.test_name_detected_objects" + + assert (state := hass.states.get(entity_id)) + assert state.state == "2" + + assert (entry := entity_registry.async_get(entity_id)) + assert entry.unique_id == "123456789ABC-presence-presence_num_objects" + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "presence", "num_objects", 0) + mock_rpc_device.mock_update() + + assert (state := hass.states.get(entity_id)) + assert state.state == "0" + + config = deepcopy(mock_rpc_device.config) + config["presence"] = {"enable": False} + monkeypatch.setattr(mock_rpc_device, "config", config) + await hass.config_entries.async_reload(mock_config_entry.entry_id) + mock_rpc_device.mock_update() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE