Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions homeassistant/components/gatus/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import override

from gatus_api import EndpointStatus
Expand All @@ -15,6 +16,7 @@
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util

from .coordinator import GatusConfigEntry, GatusDataUpdateCoordinator
from .entity import GatusEndpointEntity
Expand All @@ -26,7 +28,7 @@
class GatusSensorEntityDescription(SensorEntityDescription):
"""Class describing Gatus sensor entities."""

value_fn: Callable[[EndpointStatus], float | int | str | None]
value_fn: Callable[[EndpointStatus], datetime | float | int | str | None]


SENSOR_TYPES: tuple[GatusSensorEntityDescription, ...] = (
Expand Down Expand Up @@ -60,6 +62,20 @@ class GatusSensorEntityDescription(SensorEntityDescription):
endpoint.events[-1].type.lower() if endpoint.events else None
),
),
GatusSensorEntityDescription(
key="domain_expiration",
translation_key="domain_expiration",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda endpoint: (
dt_util.utcnow().replace(microsecond=0, second=0)
+ timedelta(
seconds=int(endpoint.results[-1].domain_expiration / 1_000_000_000)
)
if endpoint.results and endpoint.results[-1].domain_expiration is not None
else None
),
),
)


Expand All @@ -73,8 +89,10 @@ async def async_setup_entry(

async_add_entities(
GatusEndpointSensor(coordinator, entry, endpoint_key, description)
for endpoint_key in coordinator.data
for endpoint_key, endpoint in coordinator.data.items()
for description in SENSOR_TYPES
if description.key != "domain_expiration"
or (endpoint.results and endpoint.results[-1].domain_expiration is not None)
)


Expand All @@ -93,10 +111,11 @@ def __init__(
"""Initialize the sensor."""
super().__init__(coordinator, entry, endpoint_key)
self.entity_description = description
self._attr_translation_key = description.translation_key
self._attr_unique_id = f"{entry.entry_id}_{endpoint_key}_{description.key}"

@property
@override
def native_value(self) -> float | int | str | None:
def native_value(self) -> datetime | float | int | str | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.endpoint_data)
10 changes: 2 additions & 8 deletions homeassistant/components/gatus/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,8 @@
},
"entity": {
"sensor": {
"last_event": {
"name": "Last event",
"state": {
"healthy": "Healthy",
"resolved": "Resolved",
"start": "Started",
"unhealthy": "Unhealthy"
}
"domain_expiration": {
"name": "Domain expiration"
},
Comment thread
TN-1 marked this conversation as resolved.
"response_time": {
"name": "Response time"
Expand Down
9 changes: 8 additions & 1 deletion tests/components/gatus/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ def mock_gatus_client() -> Generator[AsyncMock]:
key="backend_service",
name="Backend Service",
group="Core",
results=[Result(success=True, status=200, duration=23123100)],
results=[
Result(
success=True,
status=200,
duration=23123100,
domain_expiration=7776000000000000,
)
],
events=[Event(type="HEALTHY", timestamp="2026-01-01T00:00:00Z")],
)
]
Expand Down
65 changes: 58 additions & 7 deletions tests/components/gatus/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# serializer version: 1
# name: test_sensor_setup_and_states[sensor.core_backend_service_last_event-entry]
# name: test_sensor_setup_and_states[sensor.core_backend_service-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
Expand All @@ -20,20 +20,20 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.core_backend_service_last_event',
'entity_id': 'sensor.core_backend_service',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Last event',
'object_id_base': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Last event',
'original_name': None,
'platform': 'gatus',
'previous_unique_id': None,
'suggested_object_id': None,
Expand All @@ -43,11 +43,11 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor_setup_and_states[sensor.core_backend_service_last_event-state]
# name: test_sensor_setup_and_states[sensor.core_backend_service-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Core Backend Service Last event',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Core Backend Service',
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'start',
'healthy',
Expand All @@ -56,13 +56,64 @@
]),
}),
'context': <ANY>,
'entity_id': 'sensor.core_backend_service_last_event',
'entity_id': 'sensor.core_backend_service',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'healthy',
})
# ---
# name: test_sensor_setup_and_states[sensor.core_backend_service_domain_expiration-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.core_backend_service_domain_expiration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Domain expiration',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Domain expiration',
'platform': 'gatus',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'domain_expiration',
'unique_id': '1234567890abcdef1234567890abcdef_backend_service_domain_expiration',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_setup_and_states[sensor.core_backend_service_domain_expiration-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Core Backend Service Domain expiration',
}),
'context': <ANY>,
'entity_id': 'sensor.core_backend_service_domain_expiration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2026-04-01T00:00:00+00:00',
})
# ---
# name: test_sensor_setup_and_states[sensor.core_backend_service_response_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
Expand Down
22 changes: 15 additions & 7 deletions tests/components/gatus/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ async def test_sensor_setup_and_states(
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test standard successful setup and entity snapshots using snapshot_platform."""
freezer.move_to("2026-01-01 00:00:00+00:00")
with patch("homeassistant.components.gatus._PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(
Expand All @@ -47,6 +49,7 @@ def _to_endpoint_statuses(raw_data: list[dict[str, Any]]) -> list[EndpointStatus
success=r["success"],
status=r.get("status"),
duration=r.get("duration"),
domain_expiration=r.get("domainExpiration"),
)
for r in ep.get("results", [])
],
Expand Down Expand Up @@ -167,24 +170,29 @@ async def test_sensor_missing_status_code(
assert state.state == STATE_UNKNOWN


async def test_sensor_missing_last_event(
async def test_sensor_missing_domain_expiration(
hass: HomeAssistant,
mock_gatus_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that an endpoint missing events evaluates to STATE_UNKNOWN for last_event sensor."""
"""Test that an endpoint missing domain_expiration does not create the domain expiration sensor."""
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)],
events=[],
results=[
Result(
success=True,
status=200,
duration=12500000,
domain_expiration=None,
)
],
)
]

await setup_integration(hass, mock_config_entry)

state = hass.states.get("sensor.backend_service_last_event")
assert state is not None
assert state.state == STATE_UNKNOWN
state = hass.states.get("sensor.backend_service_domain_expiration")
assert state is None
Loading