mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add Landis+Gyr Heat Meter integration (#73363)
* Add Landis+Gyr Heat Meter integration * Add contant for better sensor config * Add test for init * Refactor some of the PR suggestions in config_flow * Apply small fix * Correct total_increasing to total * Add test for restore state * Add MWh entity that can be added as gas on the energy dashoard * Remove GJ as unit * Round MWh to 5 iso 3 digits * Update homeassistant/components/landisgyr_heat_meter/const.py * Update CODEOWNERS Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
65eb1584f7
commit
7a497c1e6e
@ -587,6 +587,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lacrosse_view/ @IceBotYT
|
/tests/components/lacrosse_view/ @IceBotYT
|
||||||
/homeassistant/components/lametric/ @robbiet480 @frenck
|
/homeassistant/components/lametric/ @robbiet480 @frenck
|
||||||
/tests/components/lametric/ @robbiet480 @frenck
|
/tests/components/lametric/ @robbiet480 @frenck
|
||||||
|
/homeassistant/components/landisgyr_heat_meter/ @vpathuis
|
||||||
|
/tests/components/landisgyr_heat_meter/ @vpathuis
|
||||||
/homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol
|
/homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol
|
||||||
/tests/components/launch_library/ @ludeeus @DurgNomis-drol
|
/tests/components/launch_library/ @ludeeus @DurgNomis-drol
|
||||||
/homeassistant/components/laundrify/ @xLarry
|
/homeassistant/components/laundrify/ @xLarry
|
||||||
|
56
homeassistant/components/landisgyr_heat_meter/__init__.py
Normal file
56
homeassistant/components/landisgyr_heat_meter/__init__.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""The Landis+Gyr Heat Meter integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ultraheat_api import HeatMeterService, UltraheatReader
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_DEVICE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
async def async_update_data():
|
||||||
|
"""Fetch data from the API."""
|
||||||
|
_LOGGER.info("Polling on %s", entry.data[CONF_DEVICE])
|
||||||
|
return await hass.async_add_executor_job(api.read)
|
||||||
|
|
||||||
|
# No automatic polling and no initial refresh of data is being done at this point,
|
||||||
|
# to prevent battery drain. The user will have to do it manually.
|
||||||
|
|
||||||
|
coordinator = DataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="ultraheat_gateway",
|
||||||
|
update_method=async_update_data,
|
||||||
|
update_interval=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
136
homeassistant/components/landisgyr_heat_meter/config_flow.py
Normal file
136
homeassistant/components/landisgyr_heat_meter/config_flow.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
"""Config flow for Landis+Gyr Heat Meter integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
from ultraheat_api import HeatMeterService, UltraheatReader
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_DEVICE
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_MANUAL_PATH = "Enter Manually"
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_DEVICE): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Ultraheat Heat Meter."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Step when setting up serial configuration."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
if user_input[CONF_DEVICE] == CONF_MANUAL_PATH:
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await self.validate_and_create_entry(dev_path)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
|
||||||
|
ports = await self.get_ports()
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Set path manually."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
dev_path = user_input[CONF_DEVICE]
|
||||||
|
try:
|
||||||
|
return await self.validate_and_create_entry(dev_path)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
|
||||||
|
schema = vol.Schema({vol.Required(CONF_DEVICE): str})
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="setup_serial_manual_path",
|
||||||
|
data_schema=schema,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def validate_and_create_entry(self, dev_path):
|
||||||
|
"""Try to connect to the device path and return an entry."""
|
||||||
|
model, device_number = await self.validate_ultraheat(dev_path)
|
||||||
|
|
||||||
|
await self.async_set_unique_id(device_number)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
data = {
|
||||||
|
CONF_DEVICE: dev_path,
|
||||||
|
"model": model,
|
||||||
|
"device_number": device_number,
|
||||||
|
}
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=model,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def validate_ultraheat(self, port: str):
|
||||||
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
|
reader = UltraheatReader(port)
|
||||||
|
heat_meter = HeatMeterService(reader)
|
||||||
|
try:
|
||||||
|
async with async_timeout.timeout(10):
|
||||||
|
# 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:
|
||||||
|
_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)
|
||||||
|
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 ""
|
||||||
|
)
|
||||||
|
formatted_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH
|
||||||
|
return formatted_ports
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
192
homeassistant/components/landisgyr_heat_meter/const.py
Normal file
192
homeassistant/components/landisgyr_heat_meter/const.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
"""Constants for the Landis+Gyr Heat Meter integration."""
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ENERGY_MEGA_WATT_HOUR, TEMP_CELSIUS, VOLUME_CUBIC_METERS
|
||||||
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
|
|
||||||
|
DOMAIN = "landisgyr_heat_meter"
|
||||||
|
|
||||||
|
GJ_TO_MWH = 0.277778 # conversion factor
|
||||||
|
|
||||||
|
HEAT_METER_SENSOR_TYPES = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="heat_usage",
|
||||||
|
icon="mdi:fire",
|
||||||
|
name="Heat usage",
|
||||||
|
native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR,
|
||||||
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="volume_usage_m3",
|
||||||
|
icon="mdi:fire",
|
||||||
|
name="Volume usage",
|
||||||
|
native_unit_of_measurement=VOLUME_CUBIC_METERS,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
),
|
||||||
|
# Diagnostic entity for debugging, this will match the value in GJ indicated on the meter's display
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="heat_usage_gj",
|
||||||
|
icon="mdi:fire",
|
||||||
|
name="Heat usage GJ",
|
||||||
|
native_unit_of_measurement="GJ",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="heat_previous_year",
|
||||||
|
icon="mdi:fire",
|
||||||
|
name="Heat usage previous year",
|
||||||
|
native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="volume_previous_year_m3",
|
||||||
|
icon="mdi:fire",
|
||||||
|
name="Volume usage previous year",
|
||||||
|
native_unit_of_measurement=VOLUME_CUBIC_METERS,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="ownership_number",
|
||||||
|
name="Ownership number",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="error_number",
|
||||||
|
name="Error number",
|
||||||
|
icon="mdi:home-alert",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="device_number",
|
||||||
|
name="Device number",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="measurement_period_minutes",
|
||||||
|
name="Measurement period minutes",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="power_max_kw",
|
||||||
|
name="Power max",
|
||||||
|
native_unit_of_measurement="kW",
|
||||||
|
icon="mdi:power-plug-outline",
|
||||||
|
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",
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="flowrate_max_m3ph",
|
||||||
|
name="Flowrate max",
|
||||||
|
native_unit_of_measurement="m3ph",
|
||||||
|
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",
|
||||||
|
icon="mdi:water-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
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",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="flow_hours",
|
||||||
|
name="Flow hours",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="fault_hours",
|
||||||
|
name="Fault hours",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="fault_hours_previous_year",
|
||||||
|
name="Fault hours previous year",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="yearly_set_day",
|
||||||
|
name="Yearly set day",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="monthly_set_day",
|
||||||
|
name="Monthly set day",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="meter_date_time",
|
||||||
|
name="Meter date time",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="measuring_range_m3ph",
|
||||||
|
name="Measuring range",
|
||||||
|
native_unit_of_measurement="m3ph",
|
||||||
|
icon="mdi:water-outline",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="settings_and_firmware",
|
||||||
|
name="Settings and firmware",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
13
homeassistant/components/landisgyr_heat_meter/manifest.json
Normal file
13
homeassistant/components/landisgyr_heat_meter/manifest.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"domain": "landisgyr_heat_meter",
|
||||||
|
"name": "Landis+Gyr Heat Meter",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter",
|
||||||
|
"requirements": ["ultraheat-api==0.4.1"],
|
||||||
|
"ssdp": [],
|
||||||
|
"zeroconf": [],
|
||||||
|
"homekit": {},
|
||||||
|
"dependencies": [],
|
||||||
|
"codeowners": ["@vpathuis"],
|
||||||
|
"iot_class": "local_polling"
|
||||||
|
}
|
108
homeassistant/components/landisgyr_heat_meter/sensor.py
Normal file
108
homeassistant/components/landisgyr_heat_meter/sensor.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""Platform for sensor integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
ATTR_STATE_CLASS,
|
||||||
|
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
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
from .const import GJ_TO_MWH, HEAT_METER_SENSOR_TYPES
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
model = entry.data["model"]
|
||||||
|
|
||||||
|
device = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, unique_id)},
|
||||||
|
manufacturer="Landis & Gyr",
|
||||||
|
model=model,
|
||||||
|
name="Landis+Gyr Heat Meter",
|
||||||
|
)
|
||||||
|
|
||||||
|
sensors = []
|
||||||
|
|
||||||
|
for description in HEAT_METER_SENSOR_TYPES:
|
||||||
|
sensors.append(HeatMeterSensor(coordinator, unique_id, description, device))
|
||||||
|
|
||||||
|
async_add_entities(sensors)
|
||||||
|
|
||||||
|
|
||||||
|
class HeatMeterSensor(CoordinatorEntity, RestoreSensor):
|
||||||
|
"""Representation of a Sensor."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator, unique_id, 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_device_info = device
|
||||||
|
self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year"))
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Call when entity about to be added to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
state = await self.async_get_last_sensor_data()
|
||||||
|
if state:
|
||||||
|
self._attr_native_value = state.native_value
|
||||||
|
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
if self.key in asdict(self.coordinator.data):
|
||||||
|
if self.device_class == SensorDeviceClass.TIMESTAMP:
|
||||||
|
self._attr_native_value = dt_util.as_utc(
|
||||||
|
asdict(self.coordinator.data)[self.key]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._attr_native_value = asdict(self.coordinator.data)[self.key]
|
||||||
|
|
||||||
|
if self.key == "heat_usage":
|
||||||
|
self._attr_native_value = convert_gj_to_mwh(
|
||||||
|
self.coordinator.data.heat_usage_gj
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.key == "heat_previous_year":
|
||||||
|
self._attr_native_value = convert_gj_to_mwh(
|
||||||
|
self.coordinator.data.heat_previous_year_gj
|
||||||
|
)
|
||||||
|
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
def convert_gj_to_mwh(gigajoule) -> float:
|
||||||
|
"""Convert GJ to MWh using the conversion value."""
|
||||||
|
return round(gigajoule * GJ_TO_MWH, 5)
|
23
homeassistant/components/landisgyr_heat_meter/strings.json
Normal file
23
homeassistant/components/landisgyr_heat_meter/strings.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"device": "Select device"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setup_serial_manual_path": {
|
||||||
|
"data": {
|
||||||
|
"device": "[%key:common::config_flow::data::usb_path%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"device": "Select device"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setup_serial_manual_path": {
|
||||||
|
"data": {
|
||||||
|
"device": "USB-device path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -197,6 +197,7 @@ FLOWS = {
|
|||||||
"kulersky",
|
"kulersky",
|
||||||
"lacrosse_view",
|
"lacrosse_view",
|
||||||
"lametric",
|
"lametric",
|
||||||
|
"landisgyr_heat_meter",
|
||||||
"launch_library",
|
"launch_library",
|
||||||
"laundrify",
|
"laundrify",
|
||||||
"lg_soundbar",
|
"lg_soundbar",
|
||||||
|
@ -2383,6 +2383,9 @@ twitchAPI==2.5.2
|
|||||||
# homeassistant.components.ukraine_alarm
|
# homeassistant.components.ukraine_alarm
|
||||||
uasiren==0.0.1
|
uasiren==0.0.1
|
||||||
|
|
||||||
|
# homeassistant.components.landisgyr_heat_meter
|
||||||
|
ultraheat-api==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
unifi-discovery==1.1.5
|
unifi-discovery==1.1.5
|
||||||
|
|
||||||
|
@ -1614,6 +1614,9 @@ twitchAPI==2.5.2
|
|||||||
# homeassistant.components.ukraine_alarm
|
# homeassistant.components.ukraine_alarm
|
||||||
uasiren==0.0.1
|
uasiren==0.0.1
|
||||||
|
|
||||||
|
# homeassistant.components.landisgyr_heat_meter
|
||||||
|
ultraheat-api==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
unifi-discovery==1.1.5
|
unifi-discovery==1.1.5
|
||||||
|
|
||||||
|
1
tests/components/landisgyr_heat_meter/__init__.py
Normal file
1
tests/components/landisgyr_heat_meter/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Landis+Gyr Heat Meter component."""
|
227
tests/components/landisgyr_heat_meter/test_config_flow.py
Normal file
227
tests/components/landisgyr_heat_meter/test_config_flow.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
"""Test the Landis + Gyr Heat Meter config flow."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import serial.tools.list_ports
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.landisgyr_heat_meter import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
|
||||||
|
|
||||||
|
|
||||||
|
def mock_serial_port():
|
||||||
|
"""Mock of a serial port."""
|
||||||
|
port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234")
|
||||||
|
port.serial_number = "1234"
|
||||||
|
port.manufacturer = "Virtual serial port"
|
||||||
|
port.device = "/dev/ttyUSB1234"
|
||||||
|
port.description = "Some serial port"
|
||||||
|
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MockUltraheatRead:
|
||||||
|
"""Mock of the response from the read method of the Ultraheat API."""
|
||||||
|
|
||||||
|
model: str
|
||||||
|
device_number: str
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
|
||||||
|
async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None:
|
||||||
|
"""Test manual entry."""
|
||||||
|
|
||||||
|
mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789")
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"device": "Enter Manually"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "setup_serial_manual_path"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.landisgyr_heat_meter.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"device": "/dev/ttyUSB0"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "LUGCUH50"
|
||||||
|
assert result["data"] == {
|
||||||
|
"device": "/dev/ttyUSB0",
|
||||||
|
"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_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> None:
|
||||||
|
"""Test select from list entry."""
|
||||||
|
|
||||||
|
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"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"device": port.device}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_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")
|
||||||
|
async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None:
|
||||||
|
"""Test manual entry fails."""
|
||||||
|
|
||||||
|
mock_heat_meter().read.side_effect = Exception
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"device": "Enter Manually"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "setup_serial_manual_path"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.landisgyr_heat_meter.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"device": "/dev/ttyUSB0"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "setup_serial_manual_path"
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService")
|
||||||
|
@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
|
||||||
|
port = mock_serial_port()
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {"device": port.device}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
@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_realpath(
|
||||||
|
mock_port, mock_heat_meter, hass: HomeAssistant
|
||||||
|
) -> None:
|
||||||
|
"""Test getting the serial path name."""
|
||||||
|
|
||||||
|
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"] == RESULT_TYPE_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"] == RESULT_TYPE_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}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_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"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "LUGCUH50"
|
||||||
|
assert result["data"] == {
|
||||||
|
"device": port.device,
|
||||||
|
"model": "LUGCUH50",
|
||||||
|
"device_number": "123456789",
|
||||||
|
}
|
22
tests/components/landisgyr_heat_meter/test_init.py
Normal file
22
tests/components/landisgyr_heat_meter/test_init.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Test the Landis + Gyr Heat Meter init."""
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_DEVICE
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unload_entry(hass):
|
||||||
|
"""Test removing config entry."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain="landisgyr_heat_meter",
|
||||||
|
title="LUGCUH50",
|
||||||
|
data={CONF_DEVICE: "/dev/1234"},
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(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)
|
200
tests/components/landisgyr_heat_meter/test_sensor.py
Normal file
200
tests/components/landisgyr_heat_meter/test_sensor.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
"""The tests for the Landis+Gyr Heat Meter sensor platform."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import datetime
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.homeassistant import (
|
||||||
|
DOMAIN as HA_DOMAIN,
|
||||||
|
SERVICE_UPDATE_ENTITY,
|
||||||
|
)
|
||||||
|
from homeassistant.components.landisgyr_heat_meter.const import DOMAIN
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
ATTR_LAST_RESET,
|
||||||
|
ATTR_STATE_CLASS,
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_ICON,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
ENERGY_MEGA_WATT_HOUR,
|
||||||
|
VOLUME_CUBIC_METERS,
|
||||||
|
)
|
||||||
|
from homeassistant.core import CoreState, State
|
||||||
|
from homeassistant.helpers import entity_registry
|
||||||
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MockHeatMeterResponse:
|
||||||
|
"""Mock for HeatMeterResponse."""
|
||||||
|
|
||||||
|
heat_usage_gj: int
|
||||||
|
volume_usage_m3: int
|
||||||
|
heat_previous_year_gj: int
|
||||||
|
device_number: str
|
||||||
|
meter_date_time: datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService")
|
||||||
|
async def test_create_sensors(mock_heat_meter, hass):
|
||||||
|
"""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)
|
||||||
|
|
||||||
|
mock_heat_meter_response = MockHeatMeterResponse(
|
||||||
|
heat_usage_gj=123,
|
||||||
|
volume_usage_m3=456,
|
||||||
|
heat_previous_year_gj=111,
|
||||||
|
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 26 attributes have been created
|
||||||
|
assert len(hass.states.async_all()) == 26
|
||||||
|
entity_reg = entity_registry.async_get(hass)
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.heat_meter_heat_usage")
|
||||||
|
assert state
|
||||||
|
assert state.state == "34.16669"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR
|
||||||
|
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL
|
||||||
|
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.heat_meter_volume_usage")
|
||||||
|
assert state
|
||||||
|
assert state.state == "456"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
|
||||||
|
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.heat_meter_device_number")
|
||||||
|
assert state
|
||||||
|
assert state.state == "devicenr_789"
|
||||||
|
assert state.attributes.get(ATTR_STATE_CLASS) is None
|
||||||
|
entity_registry_entry = entity_reg.async_get("sensor.heat_meter_device_number")
|
||||||
|
assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.heat_meter_meter_date_time")
|
||||||
|
assert state
|
||||||
|
assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline"
|
||||||
|
assert state.attributes.get(ATTR_STATE_CLASS) is None
|
||||||
|
entity_registry_entry = entity_reg.async_get("sensor.heat_meter_meter_date_time")
|
||||||
|
assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService")
|
||||||
|
async def test_restore_state(mock_heat_meter, hass):
|
||||||
|
"""Test sensor restore state."""
|
||||||
|
# Home assistant is not running yet
|
||||||
|
hass.state = CoreState.not_running
|
||||||
|
last_reset = "2022-07-01T00:00:00.000000+00:00"
|
||||||
|
mock_restore_cache_with_extra_data(
|
||||||
|
hass,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
State(
|
||||||
|
"sensor.heat_meter_heat_usage",
|
||||||
|
"34167",
|
||||||
|
attributes={
|
||||||
|
ATTR_LAST_RESET: last_reset,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT: ENERGY_MEGA_WATT_HOUR,
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.TOTAL,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"native_value": 34167,
|
||||||
|
"native_unit_of_measurement": ENERGY_MEGA_WATT_HOUR,
|
||||||
|
"icon": "mdi:fire",
|
||||||
|
"last_reset": last_reset,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
State(
|
||||||
|
"sensor.heat_meter_volume_usage",
|
||||||
|
"456",
|
||||||
|
attributes={
|
||||||
|
ATTR_LAST_RESET: last_reset,
|
||||||
|
ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS,
|
||||||
|
ATTR_STATE_CLASS: SensorStateClass.TOTAL,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"native_value": 456,
|
||||||
|
"native_unit_of_measurement": VOLUME_CUBIC_METERS,
|
||||||
|
"icon": "mdi:fire",
|
||||||
|
"last_reset": last_reset,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
State(
|
||||||
|
"sensor.heat_meter_device_number",
|
||||||
|
"devicenr_789",
|
||||||
|
attributes={
|
||||||
|
ATTR_LAST_RESET: last_reset,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"native_value": "devicenr_789",
|
||||||
|
"native_unit_of_measurement": None,
|
||||||
|
"last_reset": last_reset,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
entry_data = {
|
||||||
|
"device": "/dev/USB0",
|
||||||
|
"model": "LUGCUH50",
|
||||||
|
"device_number": "123456789",
|
||||||
|
}
|
||||||
|
|
||||||
|
# create and add entry
|
||||||
|
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
|
||||||
|
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
|
||||||
|
state = hass.states.get("sensor.heat_meter_heat_usage")
|
||||||
|
assert state
|
||||||
|
assert state.state == "34167"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR
|
||||||
|
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.heat_meter_volume_usage")
|
||||||
|
assert state
|
||||||
|
assert state.state == "456"
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS
|
||||||
|
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL
|
||||||
|
|
||||||
|
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…
x
Reference in New Issue
Block a user