mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Add uptime sensor to Glances (#111402)
* Add uptime sensor to Glances * Merge upstream * Merge upstream * Fix coverage * Add uptime sensor to Glances * Merge upstream * Merge upstream * Fix coverage * Move most uptime specific code to DataUpdateCoordinator * Add last_reported after merge with upstream * Add unit tests for uptime sensor * Add unit tests for uptime sensor * Add unit tests for uptime sensor * Add unit tests for uptime sensor * Move update code out of getter native_value() * Add unit tests for uptime sensor * Update uptime method signatures * Set uptime icon in icons.json * Use freezer.tick for uptime tests * Frozen time test fails on github * Add MIN_UPTIME_VARIATION const value * Only update uptime on startup or when remote server restarts * Fix for 0 values * Set value to None to set state to Unknown if key is not found * Add unit test for uptime change * Code reduction --------- Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
parent
8ebdd46509
commit
7919ca63d0
@ -1,5 +1,6 @@
|
||||
"""Coordinator for Glances integration."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@ -10,6 +11,7 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.dt import parse_duration, utcnow
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
@ -42,4 +44,16 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except exceptions.GlancesApiError as err:
|
||||
raise UpdateFailed from err
|
||||
# Update computed values
|
||||
uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None
|
||||
up_duration: timedelta | None = None
|
||||
if up_duration := parse_duration(data.get("uptime")):
|
||||
# Update uptime if previous value is None or previous uptime is bigger than
|
||||
# new uptime (i.e. server restarted)
|
||||
if (
|
||||
self.data is None
|
||||
or self.data["computed"]["uptime_duration"] > up_duration
|
||||
):
|
||||
uptime = utcnow() - up_duration
|
||||
data["computed"] = {"uptime_duration": up_duration, "uptime": uptime}
|
||||
return data or {}
|
||||
|
@ -45,6 +45,9 @@
|
||||
},
|
||||
"raid_used": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"uptime": {
|
||||
"default": "mdi:clock-time-eight-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@ -19,7 +17,7 @@ from homeassistant.const import (
|
||||
UnitOfInformation,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@ -212,6 +210,12 @@ SENSOR_TYPES = {
|
||||
translation_key="raid_used",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("computed", "uptime"): GlancesSensorEntityDescription(
|
||||
key="uptime",
|
||||
type="computed",
|
||||
translation_key="uptime",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -276,6 +280,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.entry_id}-{sensor_label}-{description.key}"
|
||||
)
|
||||
self._update_native_value()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@ -289,13 +294,18 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
|
||||
)
|
||||
return False
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the resources."""
|
||||
value = self.coordinator.data[self.entity_description.type]
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._update_native_value()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
if isinstance(value.get(self._sensor_label), dict):
|
||||
return cast(
|
||||
StateType, value[self._sensor_label][self.entity_description.key]
|
||||
)
|
||||
return cast(StateType, value[self.entity_description.key])
|
||||
def _update_native_value(self) -> None:
|
||||
"""Update sensor native value from coordinator data."""
|
||||
data = self.coordinator.data[self.entity_description.type]
|
||||
if dict_val := data.get(self._sensor_label):
|
||||
self._attr_native_value = dict_val.get(self.entity_description.key)
|
||||
elif self.entity_description.key in data:
|
||||
self._attr_native_value = data.get(self.entity_description.key)
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
|
@ -100,6 +100,9 @@
|
||||
},
|
||||
"raid_used": {
|
||||
"name": "{sensor_label} used"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Uptime"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Tests for Glances."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
MOCK_USER_INPUT: dict[str, Any] = {
|
||||
@ -173,6 +174,8 @@ MOCK_DATA = {
|
||||
"uptime": "3 days, 10:25:20",
|
||||
}
|
||||
|
||||
MOCK_REFERENCE_DATE: datetime = datetime.fromisoformat("2024-02-13T14:13:12")
|
||||
|
||||
HA_SENSOR_DATA: dict[str, Any] = {
|
||||
"fs": {
|
||||
"/ssl": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5},
|
||||
@ -207,4 +210,5 @@ HA_SENSOR_DATA: dict[str, Any] = {
|
||||
"config": "UU",
|
||||
},
|
||||
},
|
||||
"uptime": "3 days, 10:25:20",
|
||||
}
|
||||
|
@ -954,3 +954,50 @@
|
||||
'state': '30.7',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states[sensor.0_0_0_0_uptime-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.0_0_0_0_uptime',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Uptime',
|
||||
'platform': 'glances',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'uptime',
|
||||
'unique_id': 'test--uptime',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states[sensor.0_0_0_0_uptime-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': '0.0.0.0 Uptime',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.0_0_0_0_uptime',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2024-02-10T03:47:52+00:00',
|
||||
})
|
||||
# ---
|
||||
|
@ -1,25 +1,36 @@
|
||||
"""Tests for glances sensors."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.glances.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import MOCK_USER_INPUT
|
||||
from . import HA_SENSOR_DATA, MOCK_REFERENCE_DATE, MOCK_USER_INPUT
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_sensor_states(
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test sensor states are correctly collected from library."""
|
||||
|
||||
freezer.move_to(MOCK_REFERENCE_DATE)
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test")
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
|
||||
|
||||
assert entity_entries
|
||||
@ -28,3 +39,35 @@ async def test_sensor_states(
|
||||
assert hass.states.get(entity_entry.entity_id) == snapshot(
|
||||
name=f"{entity_entry.entity_id}-state"
|
||||
)
|
||||
|
||||
|
||||
async def test_uptime_variation(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_api: AsyncMock
|
||||
) -> None:
|
||||
"""Test uptime small variation update."""
|
||||
|
||||
# Init with reference time
|
||||
freezer.move_to(MOCK_REFERENCE_DATE)
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, entry_id="test")
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
uptime_state = hass.states.get("sensor.0_0_0_0_uptime").state
|
||||
|
||||
# Time change should not change uptime (absolute date)
|
||||
freezer.tick(delta=timedelta(seconds=120))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
uptime_state2 = hass.states.get("sensor.0_0_0_0_uptime").state
|
||||
assert uptime_state2 == uptime_state
|
||||
|
||||
mock_data = HA_SENSOR_DATA.copy()
|
||||
mock_data["uptime"] = "1:25:20"
|
||||
mock_api.return_value.get_ha_sensor_data = AsyncMock(return_value=mock_data)
|
||||
|
||||
# Server has been restarted so therefore we should have a new state
|
||||
freezer.move_to(MOCK_REFERENCE_DATE + timedelta(days=2))
|
||||
freezer.tick(delta=timedelta(seconds=120))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("sensor.0_0_0_0_uptime").state == "2024-02-15T12:49:52+00:00"
|
||||
|
Loading…
x
Reference in New Issue
Block a user