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 dataclasses import dataclass, field
from datetime import date
import ipaddress
import logging
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.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
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@ -117,6 +121,15 @@ class ValloxState:
raise ValueError
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):
"""The DataUpdateCoordinator for Vallox."""

View File

@ -2,7 +2,7 @@
"domain": "vallox",
"name": "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-"],
"config_flow": true,
"iot_class": "local_polling",

View File

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

View File

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

View File

@ -1495,7 +1495,7 @@ url-normalize==1.4.1
uvcclient==0.11.0
# homeassistant.components.vallox
vallox-websocket-api==2.9.0
vallox-websocket-api==2.11.0
# homeassistant.components.rdw
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(),
)