Landis+Gyr Heat Meter code improvements (#81184)

pull/81864/head
Vincent Knoop Pathuis 2022-11-09 13:45:28 +01:00 committed by GitHub
parent 2eb37f527a
commit 0cd9fe3288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 218 additions and 168 deletions

View File

@ -4,11 +4,12 @@ from __future__ import annotations
from datetime import timedelta
import logging
from ultraheat_api import HeatMeterService, UltraheatReader
import ultraheat_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, Platform
from homeassistant.core import HomeAssistant
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
@ -22,13 +23,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up heat meter from a config entry."""
_LOGGER.debug("Initializing %s integration on %s", DOMAIN, entry.data[CONF_DEVICE])
reader = UltraheatReader(entry.data[CONF_DEVICE])
api = HeatMeterService(reader)
reader = ultraheat_api.UltraheatReader(entry.data[CONF_DEVICE])
api = ultraheat_api.HeatMeterService(reader)
async def async_update_data():
"""Fetch data from the API."""
_LOGGER.info("Polling on %s", entry.data[CONF_DEVICE])
_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.
@ -53,3 +53,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
# Removing domain name and config entry id from entity unique id's, replacing it with device number
if config_entry.version == 1:
config_entry.version = 2
device_number = config_entry.data["device_number"]
@callback
def update_entity_unique_id(entity_entry):
"""Update unique ID of entity entry."""
if entity_entry.platform in entity_entry.unique_id:
return {
"new_unique_id": entity_entry.unique_id.replace(
f"{entity_entry.platform}_{entity_entry.config_entry_id}",
f"{device_number}",
)
}
await async_migrate_entries(
hass, config_entry.entry_id, update_entity_unique_id
)
hass.config_entries.async_update_entry(config_entry)
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True

View File

@ -1,17 +1,21 @@
"""Config flow for Landis+Gyr Heat Meter integration."""
from __future__ import annotations
import asyncio
import logging
import os
from typing import Any
import async_timeout
import serial
import serial.tools.list_ports
from ultraheat_api import HeatMeterService, UltraheatReader
from serial.tools import list_ports
import ultraheat_api
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import usb
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN, ULTRAHEAT_TIMEOUT
@ -30,9 +34,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ultraheat Heat Meter."""
VERSION = 1
VERSION = 2
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Step when setting up serial configuration."""
errors = {}
@ -41,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_setup_serial_manual_path()
dev_path = await self.hass.async_add_executor_job(
get_serial_by_id, user_input[CONF_DEVICE]
usb.get_serial_by_id, user_input[CONF_DEVICE]
)
_LOGGER.debug("Using this path : %s", dev_path)
@ -50,12 +56,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except CannotConnect:
errors["base"] = "cannot_connect"
ports = await self.get_ports()
ports = await get_usb_ports(self.hass)
ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(ports)})
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_setup_serial_manual_path(self, user_input=None):
async def async_step_setup_serial_manual_path(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Set path manually."""
errors = {}
@ -78,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
model, device_number = await self.validate_ultraheat(dev_path)
_LOGGER.debug("Got model %s and device_number %s", model, device_number)
await self.async_set_unique_id(device_number)
await self.async_set_unique_id(f"{device_number}")
self._abort_if_unique_id_configured()
data = {
CONF_DEVICE: dev_path,
@ -90,48 +99,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data=data,
)
async def validate_ultraheat(self, port: str):
async def validate_ultraheat(self, port: str) -> tuple[str, str]:
"""Validate the user input allows us to connect."""
reader = UltraheatReader(port)
heat_meter = HeatMeterService(reader)
reader = ultraheat_api.UltraheatReader(port)
heat_meter = ultraheat_api.HeatMeterService(reader)
try:
async with async_timeout.timeout(ULTRAHEAT_TIMEOUT):
# validate and retrieve the model and device number for a unique id
data = await self.hass.async_add_executor_job(heat_meter.read)
_LOGGER.debug("Got data from Ultraheat API: %s", data)
except Exception as err:
except (asyncio.TimeoutError, serial.serialutil.SerialException) as err:
_LOGGER.warning("Failed read data from: %s. %s", port, err)
raise CannotConnect(f"Error communicating with device: {err}") from err
_LOGGER.debug("Successfully connected to %s", port)
_LOGGER.debug("Successfully connected to %s. Got data: %s", port, data)
return data.model, data.device_number
async def get_ports(self) -> dict:
"""Get the available ports."""
ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
formatted_ports = {}
for port in ports:
formatted_ports[
port.device
] = f"{port}, s/n: {port.serial_number or 'n/a'}" + (
f" - {port.manufacturer}" if port.manufacturer else ""
async def get_usb_ports(hass: HomeAssistant) -> dict[str, str]:
"""Return a dict of USB ports and their friendly names."""
ports = await hass.async_add_executor_job(list_ports.comports)
port_descriptions = {}
for port in ports:
# this prevents an issue with usb_device_from_port not working for ports without vid on RPi
if port.vid:
usb_device = usb.usb_device_from_port(port)
dev_path = usb.get_serial_by_id(usb_device.device)
human_name = usb.human_readable_device_name(
dev_path,
usb_device.serial_number,
usb_device.manufacturer,
usb_device.description,
usb_device.vid,
usb_device.pid,
)
formatted_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
return formatted_ports
port_descriptions[dev_path] = human_name
def get_serial_by_id(dev_path: str) -> str:
"""Return a /dev/serial/by-id match for given device if available."""
by_id = "/dev/serial/by-id"
if not os.path.isdir(by_id):
return dev_path
for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
if os.path.realpath(path) == dev_path:
return path
return dev_path
return port_descriptions
class CannotConnect(HomeAssistantError):

View File

@ -5,7 +5,15 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import ENERGY_MEGA_WATT_HOUR, TEMP_CELSIUS, VOLUME_CUBIC_METERS
from homeassistant.const import (
ENERGY_MEGA_WATT_HOUR,
POWER_KILO_WATT,
TEMP_CELSIUS,
TIME_HOURS,
TIME_MINUTES,
VOLUME_CUBIC_METERS,
VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
)
from homeassistant.helpers.entity import EntityCategory
DOMAIN = "landisgyr_heat_meter"
@ -26,6 +34,7 @@ HEAT_METER_SENSOR_TYPES = (
key="volume_usage_m3",
icon="mdi:fire",
name="Volume usage",
device_class=SensorDeviceClass.VOLUME,
native_unit_of_measurement=VOLUME_CUBIC_METERS,
state_class=SensorStateClass.TOTAL,
),
@ -56,12 +65,14 @@ HEAT_METER_SENSOR_TYPES = (
key="volume_previous_year_m3",
icon="mdi:fire",
name="Volume usage previous year",
device_class=SensorDeviceClass.VOLUME,
native_unit_of_measurement=VOLUME_CUBIC_METERS,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="ownership_number",
name="Ownership number",
icon="mdi:identifier",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
@ -73,41 +84,41 @@ HEAT_METER_SENSOR_TYPES = (
SensorEntityDescription(
key="device_number",
name="Device number",
icon="mdi:identifier",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="measurement_period_minutes",
name="Measurement period minutes",
icon="mdi:clock-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=TIME_MINUTES,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="power_max_kw",
name="Power max",
native_unit_of_measurement="kW",
icon="mdi:power-plug-outline",
native_unit_of_measurement=POWER_KILO_WATT,
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="power_max_previous_year_kw",
name="Power max previous year",
native_unit_of_measurement="kW",
icon="mdi:power-plug-outline",
native_unit_of_measurement=POWER_KILO_WATT,
device_class=SensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="flowrate_max_m3ph",
name="Flowrate max",
native_unit_of_measurement="m3ph",
native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
icon="mdi:water-outline",
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="flowrate_max_previous_year_m3ph",
name="Flowrate max previous year",
native_unit_of_measurement="m3ph",
native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
icon="mdi:water-outline",
entity_category=EntityCategory.DIAGNOSTIC,
),
@ -115,7 +126,6 @@ HEAT_METER_SENSOR_TYPES = (
key="return_temperature_max_c",
name="Return temperature max",
native_unit_of_measurement=TEMP_CELSIUS,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
),
@ -123,7 +133,6 @@ HEAT_METER_SENSOR_TYPES = (
key="return_temperature_max_previous_year_c",
name="Return temperature max previous year",
native_unit_of_measurement=TEMP_CELSIUS,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
),
@ -131,7 +140,6 @@ HEAT_METER_SENSOR_TYPES = (
key="flow_temperature_max_c",
name="Flow temperature max",
native_unit_of_measurement=TEMP_CELSIUS,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
),
@ -139,32 +147,35 @@ HEAT_METER_SENSOR_TYPES = (
key="flow_temperature_max_previous_year_c",
name="Flow temperature max previous year",
native_unit_of_measurement=TEMP_CELSIUS,
icon="mdi:thermometer",
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="operating_hours",
name="Operating hours",
icon="mdi:clock-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=TIME_HOURS,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="flow_hours",
name="Flow hours",
icon="mdi:clock-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=TIME_HOURS,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="fault_hours",
name="Fault hours",
icon="mdi:clock-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=TIME_HOURS,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="fault_hours_previous_year",
name="Fault hours previous year",
icon="mdi:clock-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=TIME_HOURS,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
@ -189,7 +200,7 @@ HEAT_METER_SENSOR_TYPES = (
SensorEntityDescription(
key="measuring_range_m3ph",
name="Measuring range",
native_unit_of_measurement="m3ph",
native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR,
icon="mdi:water-outline",
entity_category=EntityCategory.DIAGNOSTIC,
),

View File

@ -9,5 +9,6 @@
"homekit": {},
"dependencies": [],
"codeowners": ["@vpathuis"],
"dependencies": ["usb"],
"iot_class": "local_polling"
}

View File

@ -4,13 +4,8 @@ from __future__ import annotations
from dataclasses import asdict
import logging
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
RestoreSensor,
SensorDeviceClass,
)
from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -27,8 +22,6 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the sensor platform."""
_LOGGER.info("The Landis+Gyr Heat Meter sensor platform is being set up!")
unique_id = entry.entry_id
coordinator = hass.data[DOMAIN][entry.entry_id]
@ -44,7 +37,7 @@ async def async_setup_entry(
sensors = []
for description in HEAT_METER_SENSOR_TYPES:
sensors.append(HeatMeterSensor(coordinator, unique_id, description, device))
sensors.append(HeatMeterSensor(coordinator, description, device))
async_add_entities(sensors)
@ -52,24 +45,16 @@ async def async_setup_entry(
class HeatMeterSensor(CoordinatorEntity, RestoreSensor):
"""Representation of a Sensor."""
def __init__(self, coordinator, unique_id, description, device):
def __init__(self, coordinator, description, device):
"""Set up the sensor with the initial values."""
super().__init__(coordinator)
self.key = description.key
self._attr_unique_id = f"{DOMAIN}_{unique_id}_{description.key}"
self._attr_name = "Heat Meter " + description.name
if hasattr(description, "icon"):
self._attr_icon = description.icon
if hasattr(description, "entity_category"):
self._attr_entity_category = description.entity_category
if hasattr(description, ATTR_STATE_CLASS):
self._attr_state_class = description.state_class
if hasattr(description, ATTR_DEVICE_CLASS):
self._attr_device_class = description.device_class
if hasattr(description, ATTR_UNIT_OF_MEASUREMENT):
self._attr_native_unit_of_measurement = (
description.native_unit_of_measurement
)
self._attr_unique_id = (
f"{coordinator.config_entry.data['device_number']}_{description.key}"
)
self._attr_name = f"Heat Meter {description.name}"
self.entity_description = description
self._attr_device_info = device
self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year"))

View File

@ -12,10 +12,6 @@
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}

View File

@ -1,7 +1,8 @@
"""Test the Landis + Gyr Heat Meter config flow."""
from dataclasses import dataclass
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import serial
import serial.tools.list_ports
from homeassistant import config_entries
@ -9,6 +10,10 @@ from homeassistant.components.landisgyr_heat_meter import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
API_HEAT_METER_SERVICE = "homeassistant.components.landisgyr_heat_meter.config_flow.ultraheat_api.HeatMeterService"
def mock_serial_port():
"""Mock of a serial port."""
@ -17,6 +22,8 @@ def mock_serial_port():
port.manufacturer = "Virtual serial port"
port.device = "/dev/ttyUSB1234"
port.description = "Some serial port"
port.pid = 9876
port.vid = 5678
return port
@ -29,7 +36,7 @@ class MockUltraheatRead:
device_number: str
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
@patch(API_HEAT_METER_SERVICE)
async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None:
"""Test manual entry."""
@ -67,7 +74,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None:
}
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
@patch(API_HEAT_METER_SERVICE)
@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()])
async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> None:
"""Test select from list entry."""
@ -94,11 +101,11 @@ async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> No
}
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
@patch(API_HEAT_METER_SERVICE)
async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None:
"""Test manual entry fails."""
mock_heat_meter().read.side_effect = Exception
mock_heat_meter().read.side_effect = serial.serialutil.SerialException
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -128,12 +135,12 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None:
assert result["errors"] == {"base": "cannot_connect"}
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
@patch(API_HEAT_METER_SERVICE)
@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()])
async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) -> None:
"""Test select from list entry fails."""
mock_heat_meter().read.side_effect = Exception
mock_heat_meter().read.side_effect = serial.serialutil.SerialException
port = mock_serial_port()
result = await hass.config_entries.flow.async_init(
@ -151,77 +158,36 @@ async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant)
assert result["errors"] == {"base": "cannot_connect"}
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
@patch(API_HEAT_METER_SERVICE)
@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()])
async def test_get_serial_by_id_realpath(
async def test_already_configured(
mock_port, mock_heat_meter, hass: HomeAssistant
) -> None:
"""Test getting the serial path name."""
"""Test we abort if the Heat Meter is already configured."""
# create and add existing entry
entry_data = {
"device": "/dev/USB0",
"model": "LUGCUH50",
"device_number": "123456789",
}
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="123456789", data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
# run flow and see if it aborts
mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789")
port = mock_serial_port()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
scandir = [MagicMock(), MagicMock()]
scandir[0].path = "/dev/ttyUSB1234"
scandir[0].is_symlink.return_value = True
scandir[1].path = "/dev/ttyUSB5678"
scandir[1].is_symlink.return_value = True
with patch("os.path") as path:
with patch("os.scandir", return_value=scandir):
path.isdir.return_value = True
path.realpath.side_effect = ["/dev/ttyUSB1234", "/dev/ttyUSB5678"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"device": port.device}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "LUGCUH50"
assert result["data"] == {
"device": port.device,
"model": "LUGCUH50",
"device_number": "123456789",
}
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()])
async def test_get_serial_by_id_dev_path(
mock_port, mock_heat_meter, hass: HomeAssistant
) -> None:
"""Test getting the serial path name with no realpath result."""
mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789")
port = mock_serial_port()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"device": port.device}
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
scandir = [MagicMock()]
scandir[0].path.return_value = "/dev/serial/by-id/USB5678"
scandir[0].is_symlink.return_value = True
with patch("os.path") as path:
with patch("os.scandir", return_value=scandir):
path.isdir.return_value = True
path.realpath.side_effect = ["/dev/ttyUSB5678"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"device": port.device}
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == "LUGCUH50"
assert result["data"] == {
"device": port.device,
"model": "LUGCUH50",
"device_number": "123456789",
}
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -1,22 +1,78 @@
"""Test the Landis + Gyr Heat Meter init."""
from homeassistant.const import CONF_DEVICE
from unittest.mock import patch
from homeassistant.components.landisgyr_heat_meter.const import (
DOMAIN as LANDISGYR_HEAT_METER_DOMAIN,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
API_HEAT_METER_SERVICE = (
"homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService"
)
async def test_unload_entry(hass):
@patch(API_HEAT_METER_SERVICE)
async def test_unload_entry(_, hass):
"""Test removing config entry."""
entry = MockConfigEntry(
mock_entry_data = {
"device": "/dev/USB0",
"model": "LUGCUH50",
"device_number": "12345",
}
mock_entry = MockConfigEntry(
domain="landisgyr_heat_meter",
title="LUGCUH50",
data={CONF_DEVICE: "/dev/1234"},
entry_id="987654321",
data=mock_entry_data,
)
mock_entry.add_to_hass(hass)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
assert await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert "landisgyr_heat_meter" in hass.config.components
assert await hass.config_entries.async_remove(entry.entry_id)
assert await hass.config_entries.async_remove(mock_entry.entry_id)
@patch(API_HEAT_METER_SERVICE)
async def test_migrate_entry(_, hass):
"""Test successful migration of entry data from version 1 to 2."""
mock_entry_data = {
"device": "/dev/USB0",
"model": "LUGCUH50",
"device_number": "12345",
}
mock_entry = MockConfigEntry(
domain="landisgyr_heat_meter",
title="LUGCUH50",
entry_id="987654321",
data=mock_entry_data,
)
assert mock_entry.data == mock_entry_data
assert mock_entry.version == 1
mock_entry.add_to_hass(hass)
# Create entity entry to migrate to new unique ID
registry = er.async_get(hass)
registry.async_get_or_create(
SENSOR_DOMAIN,
LANDISGYR_HEAT_METER_DOMAIN,
"landisgyr_heat_meter_987654321_measuring_range_m3ph",
suggested_object_id="heat_meter_measuring_range",
config_entry=mock_entry,
)
assert await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
assert "landisgyr_heat_meter" in hass.config.components
# Check if entity unique id is migrated successfully
assert mock_entry.version == 2
entity = registry.async_get("sensor.heat_meter_measuring_range")
assert entity.unique_id == "12345_measuring_range_m3ph"

View File

@ -42,7 +42,7 @@ class MockHeatMeterResponse:
meter_date_time: datetime.datetime
@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService")
@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService")
async def test_create_sensors(mock_heat_meter, hass):
"""Test sensor."""
entry_data = {
@ -107,7 +107,7 @@ async def test_create_sensors(mock_heat_meter, hass):
assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC
@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService")
@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService")
async def test_restore_state(mock_heat_meter, hass):
"""Test sensor restore state."""
# Home assistant is not running yet
@ -177,7 +177,6 @@ async def test_restore_state(mock_heat_meter, hass):
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await async_setup_component(hass, HA_DOMAIN, {})
await hass.async_block_till_done()
# restore from cache
@ -195,6 +194,5 @@ async def test_restore_state(mock_heat_meter, hass):
state = hass.states.get("sensor.heat_meter_device_number")
assert state
print("STATE IS: ", state)
assert state.state == "devicenr_789"
assert state.attributes.get(ATTR_STATE_CLASS) is None