From 0cd9fe3288b4f4a3f027c7462f1896617e3aaeee Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Wed, 9 Nov 2022 13:45:28 +0100 Subject: [PATCH] Landis+Gyr Heat Meter code improvements (#81184) --- .../landisgyr_heat_meter/__init__.py | 44 ++++++-- .../landisgyr_heat_meter/config_flow.py | 79 +++++++------- .../components/landisgyr_heat_meter/const.py | 45 +++++--- .../landisgyr_heat_meter/manifest.json | 1 + .../components/landisgyr_heat_meter/sensor.py | 33 ++---- .../landisgyr_heat_meter/strings.json | 4 - .../landisgyr_heat_meter/test_config_flow.py | 102 ++++++------------ .../landisgyr_heat_meter/test_init.py | 72 +++++++++++-- .../landisgyr_heat_meter/test_sensor.py | 6 +- 9 files changed, 218 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 3ef235ff8af..34724c07ca9 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -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 diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index 2e244a9a65f..f12992166fb 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -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): diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 57a8f9d9be4..7767a491f3b 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -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, ), diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index 7be3115a6d3..a20225c88b0 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -9,5 +9,6 @@ "homekit": {}, "dependencies": [], "codeowners": ["@vpathuis"], + "dependencies": ["usb"], "iot_class": "local_polling" } diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 23a6e217458..2b4fc6edea8 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -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")) diff --git a/homeassistant/components/landisgyr_heat_meter/strings.json b/homeassistant/components/landisgyr_heat_meter/strings.json index 61e170af2b3..4bae2490006 100644 --- a/homeassistant/components/landisgyr_heat_meter/strings.json +++ b/homeassistant/components/landisgyr_heat_meter/strings.json @@ -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%]" } diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index 9200a9b3d23..57638868647 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -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" diff --git a/tests/components/landisgyr_heat_meter/test_init.py b/tests/components/landisgyr_heat_meter/test_init.py index b3630fc4872..6e300ec1332 100644 --- a/tests/components/landisgyr_heat_meter/test_init.py +++ b/tests/components/landisgyr_heat_meter/test_init.py @@ -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" diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index 1a068093d0e..cbaca71e52f 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -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