Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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 TYPE_CHECKING, override

from gatus_api import Result
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[[Result], float | int | str | None]
value_fn: Callable[[Result], datetime | float | int | str | None]


SENSOR_TYPES: tuple[GatusSensorEntityDescription, ...] = (
Expand All @@ -48,6 +50,18 @@ class GatusSensorEntityDescription(SensorEntityDescription):
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda result: result.status,
),
Comment thread
TN-1 marked this conversation as resolved.
Outdated
GatusSensorEntityDescription(
key="certificate_expiration",
translation_key="certificate_expiration",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
Comment thread
TN-1 marked this conversation as resolved.
value_fn=lambda result: (
dt_util.utcnow()
+ timedelta(seconds=result.certificate_expiration / 1_000_000_000)
Comment thread
TN-1 marked this conversation as resolved.
Outdated
if result.certificate_expiration is not None
else None
),
),
)


Expand All @@ -61,8 +75,12 @@ 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 != "certificate_expiration"
or (
endpoint.results and endpoint.results[-1].certificate_expiration is not None
)
)


Expand All @@ -81,11 +99,12 @@ 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."""
if TYPE_CHECKING:
assert self.latest_result is not None
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/gatus/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
},
"entity": {
"sensor": {
"certificate_expiration": {
"name": "Certificate 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,
certificate_expiration=7776000000000000,
)
],
)
]
)
Expand Down
51 changes: 51 additions & 0 deletions tests/components/gatus/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
@@ -1,4 +1,55 @@
# serializer version: 1
# name: test_sensor_setup_and_states[sensor.core_backend_service_certificate_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_certificate_expiration',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Certificate expiration',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Certificate expiration',
'platform': 'gatus',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'certificate_expiration',
'unique_id': '1234567890abcdef1234567890abcdef_backend_service_certificate_expiration',
'unit_of_measurement': None,
})
# ---
# name: test_sensor_setup_and_states[sensor.core_backend_service_certificate_expiration-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Core Backend Service Certificate expiration',
}),
'context': <ANY>,
'entity_id': 'sensor.core_backend_service_certificate_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
31 changes: 31 additions & 0 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"),
certificate_expiration=r.get("certificateExpiration"),
)
for r in ep.get("results", [])
],
Expand Down Expand Up @@ -165,3 +168,31 @@ async def test_sensor_missing_status_code(
state = hass.states.get("sensor.backend_service_status_code")
assert state is not None
assert state.state == STATE_UNKNOWN


async def test_sensor_missing_certificate_expiration(
hass: HomeAssistant,
mock_gatus_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a result missing certificate expiration 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,
certificate_expiration=None,
)
],
)
]

await setup_integration(hass, mock_config_entry)

state = hass.states.get("sensor.backend_service_certificate_expiration")
assert state is None
Loading