Landis+Gyr Heat Meter code improvements (#81184)
parent
2eb37f527a
commit
0cd9fe3288
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -9,5 +9,6 @@
|
|||
"homekit": {},
|
||||
"dependencies": [],
|
||||
"codeowners": ["@vpathuis"],
|
||||
"dependencies": ["usb"],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -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%]"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue