From 60b6a11d4ec899ecd90f1054d1b8880a634749f9 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:51:58 +0100 Subject: [PATCH] Add last restart sensor to HomeWizard (#136763) --- homeassistant/components/homewizard/sensor.py | 28 ++++++- .../components/homewizard/strings.json | 3 + .../homewizard/snapshots/test_sensor.ambr | 83 +++++++++++++++++++ tests/components/homewizard/test_sensor.py | 10 +++ 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 02355bc6c5e..f47fcfc7ca7 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime, timedelta from typing import Final from homewizard_energy.models import CombinedModels, ExternalDevice @@ -33,6 +34,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import utcnow from . import HomeWizardConfigEntry from .const import DOMAIN @@ -48,7 +50,7 @@ class HomeWizardSensorEntityDescription(SensorEntityDescription): enabled_fn: Callable[[CombinedModels], bool] = lambda x: True has_fn: Callable[[CombinedModels], bool] - value_fn: Callable[[CombinedModels], StateType] + value_fn: Callable[[CombinedModels], StateType | datetime] @dataclass(frozen=True, kw_only=True) @@ -64,6 +66,15 @@ def to_percentage(value: float | None) -> float | None: return value * 100 if value is not None else None +def time_to_datetime(value: int | None) -> datetime | None: + """Convert seconds to datetime when value is not None.""" + return ( + utcnow().replace(microsecond=0) - timedelta(seconds=value) + if value is not None + else None + ) + + SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( HomeWizardSensorEntityDescription( key="smr_version", @@ -611,6 +622,19 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( has_fn=lambda data: data.measurement.cycles is not None, value_fn=lambda data: data.measurement.cycles, ), + HomeWizardSensorEntityDescription( + key="last_restart", + translation_key="last_restart", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_fn=( + lambda data: data.system is not None and data.system.uptime_s is not None + ), + value_fn=( + lambda data: time_to_datetime(data.system.uptime_s) if data.system else None + ), + ), ) @@ -697,7 +721,7 @@ class HomeWizardSensorEntity(HomeWizardEntity, SensorEntity): self._attr_entity_registry_enabled_default = False @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime | None: """Return the sensor value.""" return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/homewizard/strings.json b/homeassistant/components/homewizard/strings.json index dbaef8439d9..645c4292ae1 100644 --- a/homeassistant/components/homewizard/strings.json +++ b/homeassistant/components/homewizard/strings.json @@ -137,6 +137,9 @@ }, "state_of_charge_pct": { "name": "State of charge" + }, + "last_restart": { + "name": "Last restart" } }, "switch": { diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index df445a9ddca..622c6d8a852 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -432,6 +432,89 @@ 'state': '50.0', }) # --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_last_restart:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '5c:2f:af:ab:cd:ef', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '5c2fafabcdef', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'Plug-In Battery', + 'model_id': 'HWE-BAT', + 'name': 'Device', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.00', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_last_restart:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_last_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last restart', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_restart', + 'unique_id': 'HWE-P1_5c2fafabcdef_last_restart', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[HWE-BAT-entity_ids10][sensor.device_last_restart:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Device Last restart', + }), + 'context': , + 'entity_id': 'sensor.device_last_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-01-28T21:39:04+00:00', + }) +# --- # name: test_sensors[HWE-BAT-entity_ids10][sensor.device_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index d9698db7469..e4498d2d47a 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -19,6 +19,7 @@ pytestmark = [ ] +@pytest.mark.freeze_time("2025-01-28 21:45:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( ("device_fixture", "entity_ids"), @@ -301,6 +302,7 @@ pytestmark = [ "sensor.device_frequency", "sensor.device_power", "sensor.device_state_of_charge", + "sensor.device_last_restart", "sensor.device_voltage", ], ), @@ -449,6 +451,7 @@ async def test_sensors( [ "sensor.device_current", "sensor.device_frequency", + "sensor.device_last_restart", "sensor.device_voltage", ], ), @@ -546,6 +549,7 @@ async def test_external_sensors_unreachable( "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_last_restart", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -595,6 +599,7 @@ async def test_external_sensors_unreachable( "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_last_restart", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -651,6 +656,7 @@ async def test_external_sensors_unreachable( "sensor.device_smart_meter_model", "sensor.device_state_of_charge", "sensor.device_tariff", + "sensor.device_last_restart", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -701,6 +707,7 @@ async def test_external_sensors_unreachable( "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_last_restart", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -739,6 +746,7 @@ async def test_external_sensors_unreachable( "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_last_restart", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -790,6 +798,7 @@ async def test_external_sensors_unreachable( "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_last_restart", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -828,6 +837,7 @@ async def test_external_sensors_unreachable( "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_last_restart", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3",