diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 159c883b5c8d..9f06947dc9db 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -12,7 +12,13 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfTime +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, + UnitOfArea, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType @@ -27,6 +33,11 @@ class RoombaSensorEntityDescription(SensorEntityDescription): value_fn: Callable[[IRobotEntity], StateType] + # IRobotEntity.new_state_filter drops messages whose only reported key is + # "signal", so that a Wi-Fi update does not wake every entity. Sensors that + # actually read "signal" have to opt back in or they never refresh. + refresh_on_signal: bool = False + DOCK_SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( @@ -136,6 +147,16 @@ SENSORS: list[RoombaSensorEntityDescription] = [ value_fn=lambda self: self.last_mission, entity_registry_enabled_default=False, ), + RoombaSensorEntityDescription( + key="rssi", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + refresh_on_signal=True, + value_fn=lambda self: self.vacuum_state.get("signal", {}).get("rssi"), + ), ] @@ -177,6 +198,13 @@ class RoombaSensor(IRobotEntity, SensorEntity): super().__init__(roomba, blid) self.entity_description = entity_description + @override + def new_state_filter(self, new_state): + """Also accept Wi-Fi only messages for sensors that read them.""" + if self.entity_description.refresh_on_signal and "signal" in new_state: + return True + return super().new_state_filter(new_state) + @property @override def unique_id(self) -> str: diff --git a/tests/components/roomba/conftest.py b/tests/components/roomba/conftest.py index 7f611c419c69..b5ee56d4789a 100644 --- a/tests/components/roomba/conftest.py +++ b/tests/components/roomba/conftest.py @@ -50,6 +50,7 @@ def mock_roomba() -> Generator[AsyncMock]: "softwareVer": "3.2.1", "hardwareRev": "1.0", "bin": {"present": True, "full": False}, + "signal": {"rssi": -47, "snr": 21, "noise": -68}, } } } diff --git a/tests/components/roomba/snapshots/test_sensor.ambr b/tests/components/roomba/snapshots/test_sensor.ambr index 04f547cc3713..89b226ff1ef5 100644 --- a/tests/components/roomba/snapshots/test_sensor.ambr +++ b/tests/components/roomba/snapshots/test_sensor.ambr @@ -419,6 +419,61 @@ 'state': 'unknown', }) # --- +# name: test_entities[sensor.test_roomba_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_roomba_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Signal strength', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'roomba', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'rssi_blid123', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_entities[sensor.test_roomba_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'signal_strength', + : 'Test Roomba Signal strength', + : , + : 'dBm', + }), + 'context': , + 'entity_id': 'sensor.test_roomba_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-47', + }) +# --- # name: test_entities[sensor.test_roomba_successful_missions-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/roomba/test_sensor.py b/tests/components/roomba/test_sensor.py index 2b54e468175b..c355fc53b128 100644 --- a/tests/components/roomba/test_sensor.py +++ b/tests/components/roomba/test_sensor.py @@ -103,3 +103,35 @@ async def test_robot_without_dock_has_no_dock_sensor( await hass.async_block_till_done() assert len(_dock_tank_level_entities(hass)) == 1 + + +async def test_rssi_refreshes_on_wifi_only_message( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_roomba: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test the RSSI sensor refreshes on a message that only carries signal. + + IRobotEntity.new_state_filter drops messages whose only reported key is + "signal" so a Wi-Fi update does not wake every entity. The RSSI sensor + reads exactly that key, so it has to opt back in or it never updates. + """ + entity_id = "sensor.test_roomba_signal_strength" + with patch("homeassistant.components.roomba.PLATFORMS", [Platform.SENSOR]): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_registry.async_update_entity(entity_id, disabled_by=None) + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "-47" + + # A Wi-Fi only update, which is what the robot sends most often. + mock_roomba.master_state["state"]["reported"]["signal"]["rssi"] = -62 + for call in mock_roomba.register_on_message_callback.call_args_list: + call.args[0]({"state": {"reported": {"signal": {"rssi": -62}}}}) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "-62"