diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 2f8acbc754c..033344cf491 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -540,7 +540,17 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity): data = self.coordinator.data[key] if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP: - self._attr_native_value = dateutil.parser.parse(data) + # The date could be "N/A" for certain fields (e.g., XOFFBATT), indicating there is no value yet. + if data == "N/A": + self._attr_native_value = None + return + + try: + self._attr_native_value = dateutil.parser.parse(data) + except (dateutil.parser.ParserError, OverflowError): + # If parsing fails we should mark it as unknown, with a log for further debugging. + _LOGGER.warning('Failed to parse date for %s: "%s"', key, data) + self._attr_native_value = None return self._attr_native_value, inferred_unit = infer_unit(data) diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index 2da69b89531..07ac0c90c04 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -3,7 +3,6 @@ from datetime import timedelta from unittest.mock import AsyncMock -import dateutil.parser import pytest from syrupy.assertion import SnapshotAssertion @@ -135,42 +134,64 @@ async def test_manual_update_entity( assert state.state == "15.0" -@pytest.mark.parametrize("mock_request_status", [MOCK_MINIMAL_STATUS], indirect=True) +@pytest.mark.parametrize( + ("mock_request_status", "entity_id", "known_status"), + [ + pytest.param( + # Even though the "LASTSTEST" field is not available, we should still create the entity. + MOCK_MINIMAL_STATUS, + "sensor.apc_ups_last_self_test", + MOCK_MINIMAL_STATUS | {"LASTSTEST": "1970-01-01 00:00:00 +0000"}, + id="last_self_test_missing", + ), + pytest.param( + MOCK_MINIMAL_STATUS | {"XOFFBATT": "N/A"}, + "sensor.apc_ups_transfer_from_battery", + MOCK_MINIMAL_STATUS | {"XOFFBATT": "1970-01-01 00:00:00 +0000"}, + id="xoffbatt_na", + ), + pytest.param( + MOCK_MINIMAL_STATUS | {"XOFFBATT": "invalid-time-string"}, + "sensor.apc_ups_transfer_from_battery", + MOCK_MINIMAL_STATUS | {"XOFFBATT": "1970-01-01 00:00:00 +0000"}, + id="xoffbatt_invalid_time_string", + ), + ], + indirect=["mock_request_status"], +) async def test_sensor_unknown( hass: HomeAssistant, mock_request_status: AsyncMock, + entity_id: str, + known_status: dict[str, str], ) -> None: - """Test if our integration can properly mark certain sensors as unknown when it becomes so.""" - ups_mode_id = "sensor.apc_ups_mode" - last_self_test_id = "sensor.apc_ups_last_self_test" + """Test if our integration can properly mark certain sensors as known/unknown when it becomes so.""" + base_status = mock_request_status.return_value - assert hass.states.get(ups_mode_id).state == MOCK_MINIMAL_STATUS["UPSMODE"] - # Last self test sensor should be added even if our status does not report it initially (it is - # a sensor that appears only after a periodical or manual self test is performed). - assert hass.states.get(last_self_test_id) is not None - assert hass.states.get(last_self_test_id).state == STATE_UNKNOWN + # The state should be unknown initially. + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN - # Simulate an event (a self test) such that "LASTSTEST" field is being reported, the state of - # the sensor should be properly updated with the corresponding value. - last_self_test_value = "1970-01-01 00:00:00 +0000" - mock_request_status.return_value = MOCK_MINIMAL_STATUS | { - "LASTSTEST": last_self_test_value - } + # Update to a payload that should make the entity known. + mock_request_status.return_value = known_status future = utcnow() + timedelta(minutes=2) async_fire_time_changed(hass, future) await hass.async_block_till_done() - assert ( - hass.states.get(last_self_test_id).state - == dateutil.parser.parse(last_self_test_value).isoformat() - ) - # Simulate another event (e.g., daemon restart) such that "LASTSTEST" is no longer reported. - mock_request_status.return_value = MOCK_MINIMAL_STATUS + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNKNOWN + + # Revert back to the initial status, and the state should now be unknown again. + mock_request_status.return_value = base_status future = utcnow() + timedelta(minutes=2) async_fire_time_changed(hass, future) await hass.async_block_till_done() - # The state should become unknown again. - assert hass.states.get(last_self_test_id).state == STATE_UNKNOWN + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN @pytest.mark.parametrize(("entity_key", "issue_key"), DEPRECATED_SENSORS.items())