diff --git a/homeassistant/components/gatus/sensor.py b/homeassistant/components/gatus/sensor.py index e76a6d21d752..a04022ce8081 100644 --- a/homeassistant/components/gatus/sensor.py +++ b/homeassistant/components/gatus/sensor.py @@ -33,6 +33,16 @@ class GatusSensorEntityDescription(SensorEntityDescription): ] +DNS_RCODE_MAP = { + "NOERROR": "no_error", + "FORMERR": "format_error", + "SERVFAIL": "server_failure", + "NXDOMAIN": "non_existent_domain", + "NOTIMP": "not_implemented", + "REFUSED": "refused", +} + + SENSOR_TYPES: tuple[GatusSensorEntityDescription, ...] = ( GatusSensorEntityDescription( key="response_time", @@ -79,6 +89,19 @@ SENSOR_TYPES: tuple[GatusSensorEntityDescription, ...] = ( else None ), ), + GatusSensorEntityDescription( + key="dns_rcode", + translation_key="dns_rcode", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator, endpoint: ( + DNS_RCODE_MAP.get( + endpoint.results[-1].dns_rcode, + endpoint.results[-1].dns_rcode.lower(), + ) + if endpoint.results and endpoint.results[-1].dns_rcode is not None + else None + ), + ), ) @@ -94,9 +117,16 @@ async def async_setup_entry( GatusEndpointSensor(coordinator, entry, endpoint_key, description) for endpoint_key, endpoint in coordinator.data.items() for description in SENSOR_TYPES - if description.key != "certificate_expiration" - or ( - endpoint.results and endpoint.results[-1].certificate_expiration is not None + if ( + description.key != "certificate_expiration" + or ( + endpoint.results + and endpoint.results[-1].certificate_expiration is not None + ) + ) + and ( + description.key != "dns_rcode" + or (endpoint.results and endpoint.results[-1].dns_rcode is not None) ) ) diff --git a/homeassistant/components/gatus/strings.json b/homeassistant/components/gatus/strings.json index f9b79d99244e..b4f0ba34e319 100644 --- a/homeassistant/components/gatus/strings.json +++ b/homeassistant/components/gatus/strings.json @@ -61,6 +61,17 @@ "certificate_expiration": { "name": "Certificate expiration" }, + "dns_rcode": { + "name": "DNS response code", + "state": { + "format_error": "Format error", + "no_error": "No error", + "non_existent_domain": "Non-existent domain", + "not_implemented": "Not implemented", + "refused": "Refused", + "server_failure": "Server failure" + } + }, "last_event": { "name": "Last event", "state": { diff --git a/tests/components/gatus/conftest.py b/tests/components/gatus/conftest.py index 9b95ae34da78..6a869f5a9631 100644 --- a/tests/components/gatus/conftest.py +++ b/tests/components/gatus/conftest.py @@ -47,6 +47,7 @@ def mock_gatus_client() -> Generator[AsyncMock]: status=200, duration=23123100, certificate_expiration=7776000000000000, + dns_rcode="NOERROR", ) ], events=[Event(type="HEALTHY", timestamp="2026-01-01T00:00:00Z")], diff --git a/tests/components/gatus/snapshots/test_sensor.ambr b/tests/components/gatus/snapshots/test_sensor.ambr index eb2fa149cf2f..a8b7639b4e02 100644 --- a/tests/components/gatus/snapshots/test_sensor.ambr +++ b/tests/components/gatus/snapshots/test_sensor.ambr @@ -50,6 +50,56 @@ 'state': '2026-04-01T00:00:00+00:00', }) # --- +# name: test_sensor_setup_and_states[sensor.core_backend_service_dns_response_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.core_backend_service_dns_response_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'DNS response code', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'DNS response code', + 'platform': 'gatus', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'dns_rcode', + 'unique_id': '1234567890abcdef1234567890abcdef_backend_service_dns_rcode', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_setup_and_states[sensor.core_backend_service_dns_response_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Core Backend Service DNS response code', + }), + 'context': , + 'entity_id': 'sensor.core_backend_service_dns_response_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_error', + }) +# --- # name: test_sensor_setup_and_states[sensor.core_backend_service_last_event-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/gatus/test_sensor.py b/tests/components/gatus/test_sensor.py index db20bec0dd7e..0facd887b425 100644 --- a/tests/components/gatus/test_sensor.py +++ b/tests/components/gatus/test_sensor.py @@ -50,6 +50,7 @@ def _to_endpoint_statuses(raw_data: list[dict[str, Any]]) -> list[EndpointStatus status=r.get("status"), duration=r.get("duration"), certificate_expiration=r.get("certificateExpiration"), + dns_rcode=r.get("dnsRcode"), ) for r in ep.get("results", []) ], @@ -196,3 +197,31 @@ async def test_sensor_missing_certificate_expiration( state = hass.states.get("sensor.backend_service_certificate_expiration") assert state is None + + +async def test_sensor_missing_dns_rcode( + hass: HomeAssistant, + mock_gatus_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that a result missing DNS rcode creates no entity.""" + mock_gatus_client.get_endpoints_statuses.return_value = [ + EndpointStatus( + key="backend_service", + name="Backend Service", + group=None, + results=[ + Result( + success=True, + status=200, + duration=12500000, + dns_rcode=None, + ) + ], + ) + ] + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("sensor.backend_service_dns_response_code") + assert state is None