Landis+Gyr move coordinator to own file (#89433)
* Move coordinator to own file and add test cases * Apply typing improvements from review * Remove testcase for exception during setup * Simplify unittest for failing serial connection * Readd checks in serial connection test after review
This commit is contained in:
parent
029093d0b2
commit
75bca76e68
4 changed files with 127 additions and 27 deletions
|
@ -1,19 +1,17 @@
|
|||
"""The Landis+Gyr Heat Meter integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import ultraheat_api
|
||||
from ultraheat_api.response import HeatMeterResponse
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_registry import async_migrate_entries
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import UltraheatCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,19 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
reader = ultraheat_api.UltraheatReader(entry.data[CONF_DEVICE])
|
||||
api = ultraheat_api.HeatMeterService(reader)
|
||||
|
||||
async def async_update_data() -> HeatMeterResponse:
|
||||
"""Fetch data from the API."""
|
||||
_LOGGER.debug("Polling on %s", entry.data[CONF_DEVICE])
|
||||
return await hass.async_add_executor_job(api.read)
|
||||
|
||||
# Polling is only daily to prevent battery drain.
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="ultraheat_gateway",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(days=1),
|
||||
)
|
||||
coordinator = UltraheatCoordinator(hass, api)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
"""Constants for the Landis+Gyr Heat Meter integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "landisgyr_heat_meter"
|
||||
|
||||
GJ_TO_MWH = 0.277778 # conversion factor
|
||||
ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time
|
||||
POLLING_INTERVAL = timedelta(days=1) # Polling is only daily to prevent battery drain.
|
||||
|
|
37
homeassistant/components/landisgyr_heat_meter/coordinator.py
Normal file
37
homeassistant/components/landisgyr_heat_meter/coordinator.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""Data update coordinator for the ultraheat api."""
|
||||
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
import serial
|
||||
from ultraheat_api.response import HeatMeterResponse
|
||||
from ultraheat_api.service import HeatMeterService
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import POLLING_INTERVAL, ULTRAHEAT_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UltraheatCoordinator(DataUpdateCoordinator[HeatMeterResponse]):
|
||||
"""Coordinator for getting data from the ultraheat api."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: HeatMeterService) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="ultraheat",
|
||||
update_interval=POLLING_INTERVAL,
|
||||
)
|
||||
self.api = api
|
||||
|
||||
async def _async_update_data(self) -> HeatMeterResponse:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
async with async_timeout.timeout(ULTRAHEAT_TIMEOUT):
|
||||
return await self.hass.async_add_executor_job(self.api.read)
|
||||
except (FileNotFoundError, serial.serialutil.SerialException) as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
|
@ -3,11 +3,13 @@ from dataclasses import dataclass
|
|||
import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import serial
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.components.landisgyr_heat_meter.const import DOMAIN
|
||||
from homeassistant.components.landisgyr_heat_meter.const import DOMAIN, POLLING_INTERVAL
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
ATTR_STATE_CLASS,
|
||||
|
@ -19,6 +21,7 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID,
|
||||
ATTR_ICON,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNAVAILABLE,
|
||||
EntityCategory,
|
||||
UnitOfEnergy,
|
||||
UnitOfVolume,
|
||||
|
@ -28,21 +31,29 @@ from homeassistant.helpers import entity_registry as er
|
|||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
mock_restore_cache_with_extra_data,
|
||||
)
|
||||
|
||||
API_HEAT_METER_SERVICE = (
|
||||
"homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockHeatMeterResponse:
|
||||
"""Mock for HeatMeterResponse."""
|
||||
|
||||
heat_usage_gj: int
|
||||
volume_usage_m3: int
|
||||
heat_previous_year_gj: int
|
||||
heat_usage_gj: float
|
||||
volume_usage_m3: float
|
||||
heat_previous_year_gj: float
|
||||
device_number: str
|
||||
meter_date_time: datetime.datetime
|
||||
|
||||
|
||||
@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService")
|
||||
@patch(API_HEAT_METER_SERVICE)
|
||||
async def test_create_sensors(
|
||||
mock_heat_meter, hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
|
@ -57,9 +68,9 @@ async def test_create_sensors(
|
|||
mock_entry.add_to_hass(hass)
|
||||
|
||||
mock_heat_meter_response = MockHeatMeterResponse(
|
||||
heat_usage_gj=123,
|
||||
volume_usage_m3=456,
|
||||
heat_previous_year_gj=111,
|
||||
heat_usage_gj=123.0,
|
||||
volume_usage_m3=456.0,
|
||||
heat_previous_year_gj=111.0,
|
||||
device_number="devicenr_789",
|
||||
meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 19, 41, 17)),
|
||||
)
|
||||
|
@ -89,7 +100,7 @@ async def test_create_sensors(
|
|||
|
||||
state = hass.states.get("sensor.heat_meter_volume_usage")
|
||||
assert state
|
||||
assert state.state == "456"
|
||||
assert state.state == "456.0"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL
|
||||
|
||||
|
@ -110,7 +121,7 @@ async def test_create_sensors(
|
|||
assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
|
||||
|
||||
@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService")
|
||||
@patch(API_HEAT_METER_SERVICE)
|
||||
async def test_restore_state(mock_heat_meter, hass: HomeAssistant) -> None:
|
||||
"""Test sensor restore state."""
|
||||
# Home assistant is not running yet
|
||||
|
@ -199,3 +210,66 @@ async def test_restore_state(mock_heat_meter, hass: HomeAssistant) -> None:
|
|||
assert state
|
||||
assert state.state == "devicenr_789"
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) is None
|
||||
|
||||
|
||||
@patch(API_HEAT_METER_SERVICE)
|
||||
async def test_exception_on_polling(mock_heat_meter, hass: HomeAssistant) -> None:
|
||||
"""Test sensor."""
|
||||
entry_data = {
|
||||
"device": "/dev/USB0",
|
||||
"model": "LUGCUH50",
|
||||
"device_number": "123456789",
|
||||
}
|
||||
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
# First setup normally
|
||||
mock_heat_meter_response = MockHeatMeterResponse(
|
||||
heat_usage_gj=123.0,
|
||||
volume_usage_m3=456.0,
|
||||
heat_previous_year_gj=111.0,
|
||||
device_number="devicenr_789",
|
||||
meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 19, 41, 17)),
|
||||
)
|
||||
|
||||
mock_heat_meter().read.return_value = mock_heat_meter_response
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: "sensor.heat_meter_heat_usage"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# check if initial setup succeeded
|
||||
state = hass.states.get("sensor.heat_meter_heat_usage")
|
||||
assert state
|
||||
assert state.state == "34.16669"
|
||||
|
||||
# Now 'disable' the connection and wait for polling and see if it fails
|
||||
mock_heat_meter().read.side_effect = serial.serialutil.SerialException
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + POLLING_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.heat_meter_heat_usage")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Now 'enable' and see if next poll succeeds
|
||||
mock_heat_meter_response = MockHeatMeterResponse(
|
||||
heat_usage_gj=124.0,
|
||||
volume_usage_m3=457.0,
|
||||
heat_previous_year_gj=112.0,
|
||||
device_number="devicenr_789",
|
||||
meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 20, 41, 17)),
|
||||
)
|
||||
|
||||
mock_heat_meter().read.return_value = mock_heat_meter_response
|
||||
mock_heat_meter().read.side_effect = None
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + POLLING_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.heat_meter_heat_usage")
|
||||
assert state
|
||||
assert state.state == "34.44447"
|
||||
|
|
Loading…
Add table
Reference in a new issue