Improve Vallox filter remaining time sensor (#66763)

This commit is contained in:
Sebastian Lövdahl 2022-02-22 01:17:54 +02:00 committed by GitHub
parent b19bf9b147
commit 744a2013cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 265 additions and 16 deletions

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import date
import ipaddress import ipaddress
import logging import logging
from typing import Any, NamedTuple from typing import Any, NamedTuple
@ -9,7 +10,10 @@ from uuid import UUID
from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox
from vallox_websocket_api.exceptions import ValloxApiException from vallox_websocket_api.exceptions import ValloxApiException
from vallox_websocket_api.vallox import get_uuid as calculate_uuid from vallox_websocket_api.vallox import (
get_next_filter_change_date as calculate_next_filter_change_date,
get_uuid as calculate_uuid,
)
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@ -117,6 +121,15 @@ class ValloxState:
raise ValueError raise ValueError
return uuid return uuid
def get_next_filter_change_date(self) -> date | None:
"""Return the next filter change date."""
next_filter_change_date = calculate_next_filter_change_date(self.metric_cache)
if not isinstance(next_filter_change_date, date):
return None
return next_filter_change_date
class ValloxDataUpdateCoordinator(DataUpdateCoordinator): class ValloxDataUpdateCoordinator(DataUpdateCoordinator):
"""The DataUpdateCoordinator for Vallox.""" """The DataUpdateCoordinator for Vallox."""

View File

@ -2,7 +2,7 @@
"domain": "vallox", "domain": "vallox",
"name": "Vallox", "name": "Vallox",
"documentation": "https://www.home-assistant.io/integrations/vallox", "documentation": "https://www.home-assistant.io/integrations/vallox",
"requirements": ["vallox-websocket-api==2.9.0"], "requirements": ["vallox-websocket-api==2.11.0"],
"codeowners": ["@andre-richter", "@slovdahl", "@viiru-"], "codeowners": ["@andre-richter", "@slovdahl", "@viiru-"],
"config_flow": true, "config_flow": true,
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, time
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util from homeassistant.util import dt
from . import ValloxDataUpdateCoordinator from . import ValloxDataUpdateCoordinator
from .const import ( from .const import (
@ -95,18 +95,15 @@ class ValloxFilterRemainingSensor(ValloxSensor):
@property @property
def native_value(self) -> StateType | datetime: def native_value(self) -> StateType | datetime:
"""Return the value reported by the sensor.""" """Return the value reported by the sensor."""
super_native_value = super().native_value next_filter_change_date = self.coordinator.data.get_next_filter_change_date()
if not isinstance(super_native_value, (int, float)): if next_filter_change_date is None:
return None return None
# Since only a delta of days is received from the device, fix the time so the timestamp does return datetime.combine(
# not change with every update. next_filter_change_date,
days_remaining = float(super_native_value) time(hour=13, minute=0, second=0, tzinfo=dt.DEFAULT_TIME_ZONE),
days_remaining_delta = timedelta(days=days_remaining) )
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
return (now + days_remaining_delta).astimezone(dt_util.UTC)
class ValloxCellStateSensor(ValloxSensor): class ValloxCellStateSensor(ValloxSensor):
@ -150,7 +147,6 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = (
ValloxSensorEntityDescription( ValloxSensorEntityDescription(
key="remaining_time_for_filter", key="remaining_time_for_filter",
name="Remaining Time For Filter", name="Remaining Time For Filter",
metric_key="A_CYC_REMAINING_TIME_FOR_FILTER",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
sensor_type=ValloxFilterRemainingSensor, sensor_type=ValloxFilterRemainingSensor,
), ),

View File

@ -2425,7 +2425,7 @@ uscisstatus==0.1.1
uvcclient==0.11.0 uvcclient==0.11.0
# homeassistant.components.vallox # homeassistant.components.vallox
vallox-websocket-api==2.9.0 vallox-websocket-api==2.11.0
# homeassistant.components.rdw # homeassistant.components.rdw
vehicle==0.3.1 vehicle==0.3.1

View File

@ -1495,7 +1495,7 @@ url-normalize==1.4.1
uvcclient==0.11.0 uvcclient==0.11.0
# homeassistant.components.vallox # homeassistant.components.vallox
vallox-websocket-api==2.9.0 vallox-websocket-api==2.11.0
# homeassistant.components.rdw # homeassistant.components.rdw
vehicle==0.3.1 vehicle==0.3.1

View File

@ -0,0 +1,65 @@
"""Common utilities for Vallox tests."""
import random
import string
from typing import Any
from unittest.mock import patch
from uuid import UUID
import pytest
from vallox_websocket_api.vallox import PROFILE
from homeassistant.components.vallox.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.fixture
def mock_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Create mocked Vallox config entry."""
vallox_mock_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "192.168.100.50",
CONF_NAME: "Vallox",
},
)
vallox_mock_entry.add_to_hass(hass)
return vallox_mock_entry
def patch_metrics(metrics: dict[str, Any]):
"""Patch the Vallox metrics response."""
return patch(
"homeassistant.components.vallox.Vallox.fetch_metrics",
return_value=metrics,
)
@pytest.fixture(autouse=True)
def patch_profile_home():
"""Patch the Vallox profile response."""
with patch(
"homeassistant.components.vallox.Vallox.get_profile",
return_value=PROFILE.HOME,
):
yield
@pytest.fixture(autouse=True)
def patch_uuid():
"""Patch the Vallox entity UUID."""
with patch(
"homeassistant.components.vallox.calculate_uuid",
return_value=_random_uuid(),
):
yield
def _random_uuid():
"""Generate a random UUID."""
uuid = "".join(random.choices(string.hexdigits, k=32))
return UUID(uuid)

View File

@ -0,0 +1,175 @@
"""Tests for Vallox sensor platform."""
from datetime import datetime, timedelta, tzinfo
from unittest.mock import patch
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.util import dt
from .conftest import patch_metrics
from tests.common import MockConfigEntry
ORIG_TZ = dt.DEFAULT_TIME_ZONE
@pytest.fixture(autouse=True)
def reset_tz():
"""Restore the default TZ after test runs."""
yield
dt.DEFAULT_TIME_ZONE = ORIG_TZ
@pytest.fixture
def set_tz(request):
"""Set the default TZ to the one requested."""
return request.getfixturevalue(request.param)
@pytest.fixture
def utc() -> tzinfo:
"""Set the default TZ to UTC."""
tz = dt.get_time_zone("UTC")
dt.set_default_time_zone(tz)
return tz
@pytest.fixture
def helsinki() -> tzinfo:
"""Set the default TZ to Europe/Helsinki."""
tz = dt.get_time_zone("Europe/Helsinki")
dt.set_default_time_zone(tz)
return tz
@pytest.fixture
def new_york() -> tzinfo:
"""Set the default TZ to America/New_York."""
tz = dt.get_time_zone("America/New_York")
dt.set_default_time_zone(tz)
return tz
def _sensor_to_datetime(sensor):
return datetime.fromisoformat(sensor.state)
def _now_at_13():
return dt.now().timetz().replace(hour=13, minute=0, second=0, microsecond=0)
async def test_remaining_filter_returns_timestamp(
mock_entry: MockConfigEntry, hass: HomeAssistant
):
"""Test that the remaining time for filter sensor returns a timestamp."""
# Act
with patch(
"homeassistant.components.vallox.calculate_next_filter_change_date",
return_value=dt.now().date(),
), patch_metrics(metrics={}):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
# Assert
sensor = hass.states.get("sensor.vallox_remaining_time_for_filter")
assert sensor.attributes["device_class"] == "timestamp"
async def test_remaining_time_for_filter_none_returned_from_vallox(
mock_entry: MockConfigEntry, hass: HomeAssistant
):
"""Test that the remaining time for filter sensor returns 'unknown' when Vallox returns None."""
# Act
with patch(
"homeassistant.components.vallox.calculate_next_filter_change_date",
return_value=None,
), patch_metrics(metrics={}):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
# Assert
sensor = hass.states.get("sensor.vallox_remaining_time_for_filter")
assert sensor.state == "unknown"
@pytest.mark.parametrize(
"set_tz",
[
"utc",
"helsinki",
"new_york",
],
indirect=True,
)
async def test_remaining_time_for_filter_in_the_future(
mock_entry: MockConfigEntry, set_tz: tzinfo, hass: HomeAssistant
):
"""Test remaining time for filter when Vallox returns a date in the future."""
# Arrange
remaining_days = 112
mocked_filter_end_date = dt.now().date() + timedelta(days=remaining_days)
# Act
with patch(
"homeassistant.components.vallox.calculate_next_filter_change_date",
return_value=mocked_filter_end_date,
), patch_metrics(metrics={}):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
# Assert
sensor = hass.states.get("sensor.vallox_remaining_time_for_filter")
assert _sensor_to_datetime(sensor) == datetime.combine(
mocked_filter_end_date,
_now_at_13(),
)
async def test_remaining_time_for_filter_today(
mock_entry: MockConfigEntry, hass: HomeAssistant
):
"""Test remaining time for filter when Vallox returns today."""
# Arrange
remaining_days = 0
mocked_filter_end_date = dt.now().date() + timedelta(days=remaining_days)
# Act
with patch(
"homeassistant.components.vallox.calculate_next_filter_change_date",
return_value=mocked_filter_end_date,
), patch_metrics(metrics={}):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
# Assert
sensor = hass.states.get("sensor.vallox_remaining_time_for_filter")
assert _sensor_to_datetime(sensor) == datetime.combine(
mocked_filter_end_date,
_now_at_13(),
)
async def test_remaining_time_for_filter_in_the_past(
mock_entry: MockConfigEntry, hass: HomeAssistant
):
"""Test remaining time for filter when Vallox returns a date in the past."""
# Arrange
remaining_days = -3
mocked_filter_end_date = dt.now().date() + timedelta(days=remaining_days)
# Act
with patch(
"homeassistant.components.vallox.calculate_next_filter_change_date",
return_value=mocked_filter_end_date,
), patch_metrics(metrics={}):
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
# Assert
sensor = hass.states.get("sensor.vallox_remaining_time_for_filter")
assert _sensor_to_datetime(sensor) == datetime.combine(
mocked_filter_end_date,
_now_at_13(),
)