From ea83b5a89266434c18e3fde51142198842d8d9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 12 Feb 2026 12:39:58 +0100 Subject: [PATCH] Add Homevolt battery integration (#160416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 2 + homeassistant/components/homevolt/__init__.py | 41 + .../components/homevolt/config_flow.py | 119 + homeassistant/components/homevolt/const.py | 9 + .../components/homevolt/coordinator.py | 56 + .../components/homevolt/manifest.json | 11 + .../components/homevolt/quality_scale.yaml | 70 + homeassistant/components/homevolt/sensor.py | 365 ++++ .../components/homevolt/strings.json | 147 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/homevolt/__init__.py | 1 + tests/components/homevolt/conftest.py | 109 + .../homevolt/fixtures/device_metadata.json | 6 + .../homevolt/fixtures/schedule.json | 15 + .../components/homevolt/fixtures/sensors.json | 37 + .../homevolt/snapshots/test_sensor.ambr | 1936 +++++++++++++++++ tests/components/homevolt/test_config_flow.py | 225 ++ tests/components/homevolt/test_init.py | 60 + tests/components/homevolt/test_sensor.py | 62 + 22 files changed, 3284 insertions(+) create mode 100644 homeassistant/components/homevolt/__init__.py create mode 100644 homeassistant/components/homevolt/config_flow.py create mode 100644 homeassistant/components/homevolt/const.py create mode 100644 homeassistant/components/homevolt/coordinator.py create mode 100644 homeassistant/components/homevolt/manifest.json create mode 100644 homeassistant/components/homevolt/quality_scale.yaml create mode 100644 homeassistant/components/homevolt/sensor.py create mode 100644 homeassistant/components/homevolt/strings.json create mode 100644 tests/components/homevolt/__init__.py create mode 100644 tests/components/homevolt/conftest.py create mode 100644 tests/components/homevolt/fixtures/device_metadata.json create mode 100644 tests/components/homevolt/fixtures/schedule.json create mode 100644 tests/components/homevolt/fixtures/sensors.json create mode 100644 tests/components/homevolt/snapshots/test_sensor.ambr create mode 100644 tests/components/homevolt/test_config_flow.py create mode 100644 tests/components/homevolt/test_init.py create mode 100644 tests/components/homevolt/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c1e9c39c44c..93e16bca53a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -719,6 +719,8 @@ build.json @home-assistant/supervisor /tests/components/homematic/ @pvizeli /homeassistant/components/homematicip_cloud/ @hahn-th @lackas /tests/components/homematicip_cloud/ @hahn-th @lackas +/homeassistant/components/homevolt/ @danielhiversen +/tests/components/homevolt/ @danielhiversen /homeassistant/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL /homeassistant/components/honeywell/ @rdfurman @mkmer diff --git a/homeassistant/components/homevolt/__init__.py b/homeassistant/components/homevolt/__init__.py new file mode 100644 index 00000000000..97f0d684eb8 --- /dev/null +++ b/homeassistant/components/homevolt/__init__.py @@ -0,0 +1,41 @@ +"""The Homevolt integration.""" + +from __future__ import annotations + +from homevolt import Homevolt + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool: + """Set up Homevolt from a config entry.""" + host: str = entry.data[CONF_HOST] + password: str | None = entry.data.get(CONF_PASSWORD) + + websession = async_get_clientsession(hass) + client = Homevolt(host, password, websession=websession) + + coordinator = HomevoltDataUpdateCoordinator(hass, entry, client) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + await entry.runtime_data.client.close_connection() + + return unload_ok diff --git a/homeassistant/components/homevolt/config_flow.py b/homeassistant/components/homevolt/config_flow.py new file mode 100644 index 00000000000..8e64e955e06 --- /dev/null +++ b/homeassistant/components/homevolt/config_flow.py @@ -0,0 +1,119 @@ +"""Config flow for the Homevolt integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from homevolt import Homevolt, HomevoltAuthenticationError, HomevoltConnectionError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + } +) + +STEP_CREDENTIALS_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } +) + + +class HomevoltConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Homevolt.""" + + VERSION = 1 + MINOR_VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._host: str | None = None + + async def check_status(self, client: Homevolt) -> dict[str, str]: + """Check connection status and return errors if any.""" + errors: dict[str, str] = {} + try: + await client.update_info() + except HomevoltAuthenticationError: + errors["base"] = "invalid_auth" + except HomevoltConnectionError: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Error occurred while connecting to the Homevolt battery") + errors["base"] = "unknown" + return errors + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + host = user_input[CONF_HOST] + password = None + websession = async_get_clientsession(self.hass) + client = Homevolt(host, password, websession=websession) + errors = await self.check_status(client) + if errors.get("base") == "invalid_auth": + self._host = host + return await self.async_step_credentials() + + if not errors: + device_id = client.unique_id + await self.async_set_unique_id(device_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title="Homevolt", + data={ + CONF_HOST: host, + CONF_PASSWORD: None, + }, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_credentials( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the credentials step.""" + errors: dict[str, str] = {} + assert self._host is not None + + if user_input is not None: + password = user_input[CONF_PASSWORD] + websession = async_get_clientsession(self.hass) + client = Homevolt(self._host, password, websession=websession) + errors = await self.check_status(client) + + if not errors: + device_id = client.unique_id + await self.async_set_unique_id(device_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title="Homevolt", + data={ + CONF_HOST: self._host, + CONF_PASSWORD: password, + }, + ) + + return self.async_show_form( + step_id="credentials", + data_schema=STEP_CREDENTIALS_DATA_SCHEMA, + errors=errors, + description_placeholders={"host": self._host}, + ) diff --git a/homeassistant/components/homevolt/const.py b/homeassistant/components/homevolt/const.py new file mode 100644 index 00000000000..d700bd8fc45 --- /dev/null +++ b/homeassistant/components/homevolt/const.py @@ -0,0 +1,9 @@ +"""Constants for the Homevolt integration.""" + +from __future__ import annotations + +from datetime import timedelta + +DOMAIN = "homevolt" +MANUFACTURER = "Homevolt" +SCAN_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/homevolt/coordinator.py b/homeassistant/components/homevolt/coordinator.py new file mode 100644 index 00000000000..0109d4df9f2 --- /dev/null +++ b/homeassistant/components/homevolt/coordinator.py @@ -0,0 +1,56 @@ +"""Data update coordinator for Homevolt integration.""" + +from __future__ import annotations + +import logging + +from homevolt import ( + Homevolt, + HomevoltAuthenticationError, + HomevoltConnectionError, + HomevoltError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, SCAN_INTERVAL + +type HomevoltConfigEntry = ConfigEntry[HomevoltDataUpdateCoordinator] + +_LOGGER = logging.getLogger(__name__) + + +class HomevoltDataUpdateCoordinator(DataUpdateCoordinator[Homevolt]): + """Class to manage fetching Homevolt data.""" + + config_entry: HomevoltConfigEntry + + def __init__( + self, + hass: HomeAssistant, + entry: HomevoltConfigEntry, + client: Homevolt, + ) -> None: + """Initialize the Homevolt coordinator.""" + self.client = client + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + config_entry=entry, + ) + + async def _async_update_data(self) -> Homevolt: + """Fetch data from the Homevolt API.""" + try: + await self.client.update_info() + except HomevoltAuthenticationError as err: + raise ConfigEntryAuthFailed from err + except (HomevoltConnectionError, HomevoltError) as err: + raise UpdateFailed(f"Error communicating with device: {err}") from err + + return self.client diff --git a/homeassistant/components/homevolt/manifest.json b/homeassistant/components/homevolt/manifest.json new file mode 100644 index 00000000000..5dff6edbe5d --- /dev/null +++ b/homeassistant/components/homevolt/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "homevolt", + "name": "Homevolt", + "codeowners": ["@danielhiversen"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/homevolt", + "integration_type": "device", + "iot_class": "local_polling", + "quality_scale": "bronze", + "requirements": ["homevolt==0.4.3"] +} diff --git a/homeassistant/components/homevolt/quality_scale.yaml b/homeassistant/components/homevolt/quality_scale.yaml new file mode 100644 index 00000000000..cd12e70ac12 --- /dev/null +++ b/homeassistant/components/homevolt/quality_scale.yaml @@ -0,0 +1,70 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: Integration does not register custom actions. + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: Integration does not register custom actions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: Local_polling without events + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: Integration does not register custom actions. + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: Integration does not have an options flow. + docs-installation-parameters: todo + entity-unavailable: done + integration-owner: done + log-when-unavailable: todo + parallel-updates: done + reauthentication-flow: todo + test-coverage: todo + + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: todo + entity-category: todo + entity-device-class: done + entity-disabled-by-default: todo + entity-translations: done + exception-translations: todo + icon-translations: todo + reconfiguration-flow: todo + repair-issues: todo + stale-devices: todo + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: todo diff --git a/homeassistant/components/homevolt/sensor.py b/homeassistant/components/homevolt/sensor.py new file mode 100644 index 00000000000..8662b484483 --- /dev/null +++ b/homeassistant/components/homevolt/sensor.py @@ -0,0 +1,365 @@ +"""Support for Homevolt sensors.""" + +from __future__ import annotations + +import logging + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS, + EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator + +PARALLEL_UPDATES = 0 # Coordinator-based updates + +_LOGGER = logging.getLogger(__name__) + + +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="available_charging_energy", + translation_key="available_charging_energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + ), + SensorEntityDescription( + key="available_charging_power", + translation_key="available_charging_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + ), + SensorEntityDescription( + key="available_discharge_energy", + translation_key="available_discharge_energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + ), + SensorEntityDescription( + key="available_discharge_power", + translation_key="available_discharge_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + ), + SensorEntityDescription( + key="rssi", + translation_key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="average_rssi", + translation_key="average_rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="charge_cycles", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement="cycles", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="energy_exported", + translation_key="energy_exported", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + ), + SensorEntityDescription( + key="energy_imported", + translation_key="energy_imported", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + ), + SensorEntityDescription( + key="exported_energy", + translation_key="exported_energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + ), + SensorEntityDescription( + key="frequency", + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="imported_energy", + translation_key="imported_energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + ), + SensorEntityDescription( + key="l1_current", + translation_key="l1_current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + ), + SensorEntityDescription( + key="l1_l2_voltage", + translation_key="l1_l2_voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + ), + SensorEntityDescription( + key="l1_power", + translation_key="l1_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="l1_voltage", + translation_key="l1_voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="l2_current", + translation_key="l2_current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + ), + SensorEntityDescription( + key="l2_l3_voltage", + translation_key="l2_l3_voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + ), + SensorEntityDescription( + key="l2_power", + translation_key="l2_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="l2_voltage", + translation_key="l2_voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="l3_current", + translation_key="l3_current", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + ), + SensorEntityDescription( + key="l3_l1_voltage", + translation_key="l3_l1_voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + ), + SensorEntityDescription( + key="l3_power", + translation_key="l3_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="l3_voltage", + translation_key="l3_voltage", + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="schedule_id", + translation_key="schedule_id", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="schedule_max_discharge", + translation_key="schedule_max_discharge", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="schedule_max_power", + translation_key="schedule_max_power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="schedule_power_setpoint", + translation_key="schedule_power_setpoint", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="schedule_type", + translation_key="schedule_type", + device_class=SensorDeviceClass.ENUM, + options=[ + "idle", + "inverter_charge", + "inverter_discharge", + "grid_charge", + "grid_discharge", + "grid_charge_discharge", + "frequency_reserve", + "solar_charge", + "solar_charge_discharge", + "full_solar_export", + ], + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="state_of_charge", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="system_temperature", + translation_key="system_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="tmax", + translation_key="tmax", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="tmin", + translation_key="tmin", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HomevoltConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Homevolt sensor.""" + coordinator = entry.runtime_data + entities: list[HomevoltSensor] = [] + sensors_by_key = {sensor.key: sensor for sensor in SENSORS} + for sensor_key, sensor in coordinator.data.sensors.items(): + if (description := sensors_by_key.get(sensor.type)) is None: + _LOGGER.warning("Unsupported sensor '%s' found during setup", sensor) + continue + entities.append( + HomevoltSensor( + description, + coordinator, + sensor_key, + ) + ) + async_add_entities(entities) + + +class HomevoltSensor(CoordinatorEntity[HomevoltDataUpdateCoordinator], SensorEntity): + """Representation of a Homevolt sensor.""" + + entity_description: SensorEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + description: SensorEntityDescription, + coordinator: HomevoltDataUpdateCoordinator, + sensor_key: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description = description + unique_id = coordinator.data.unique_id + self._attr_unique_id = f"{unique_id}_{sensor_key}" + sensor_data = coordinator.data.sensors[sensor_key] + self._sensor_key = sensor_key + + device_metadata = coordinator.data.device_metadata.get( + sensor_data.device_identifier + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{unique_id}_{sensor_data.device_identifier}")}, + configuration_url=coordinator.client.base_url, + manufacturer=MANUFACTURER, + model=device_metadata.model if device_metadata else None, + name=device_metadata.name if device_metadata else None, + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self._sensor_key in self.coordinator.data.sensors + + @property + def native_value(self) -> StateType: + """Return the native value of the sensor.""" + return self.coordinator.data.sensors[self._sensor_key].value diff --git a/homeassistant/components/homevolt/strings.json b/homeassistant/components/homevolt/strings.json new file mode 100644 index 00000000000..eb8f5f9fc1a --- /dev/null +++ b/homeassistant/components/homevolt/strings.json @@ -0,0 +1,147 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "credentials": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "The local password configured for your Homevolt battery." + }, + "description": "This device requires a password to connect. Please enter the password for {host}." + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The IP address or hostname of your Homevolt battery on your local network." + }, + "description": "Connect Home Assistant to your Homevolt battery over the local network." + } + } + }, + "entity": { + "sensor": { + "available_charging_energy": { + "name": "Available charging energy" + }, + "available_charging_power": { + "name": "Available charging power" + }, + "available_discharge_energy": { + "name": "Available discharge energy" + }, + "available_discharge_power": { + "name": "Available discharge power" + }, + "average_rssi": { + "name": "Average RSSI" + }, + "battery_state_of_charge": { + "name": "Battery state of charge" + }, + "charge_cycles": { + "unit_of_measurement": "cycles" + }, + "energy_exported": { + "name": "Energy exported" + }, + "energy_imported": { + "name": "Energy imported" + }, + "exported_energy": { + "name": "Exported energy" + }, + "imported_energy": { + "name": "Imported energy" + }, + "l1_current": { + "name": "L1 current" + }, + "l1_l2_voltage": { + "name": "L1-L2 voltage" + }, + "l1_power": { + "name": "L1 power" + }, + "l1_voltage": { + "name": "L1 voltage" + }, + "l2_current": { + "name": "L2 current" + }, + "l2_l3_voltage": { + "name": "L2-L3 voltage" + }, + "l2_power": { + "name": "L2 power" + }, + "l2_voltage": { + "name": "L2 voltage" + }, + "l3_current": { + "name": "L3 current" + }, + "l3_l1_voltage": { + "name": "L3-L1 voltage" + }, + "l3_power": { + "name": "L3 power" + }, + "l3_voltage": { + "name": "L3 voltage" + }, + "power": { + "name": "Power" + }, + "rssi": { + "name": "RSSI" + }, + "schedule_id": { + "name": "Schedule ID" + }, + "schedule_max_discharge": { + "name": "Schedule max discharge" + }, + "schedule_max_power": { + "name": "Schedule max power" + }, + "schedule_power_setpoint": { + "name": "Schedule power setpoint" + }, + "schedule_type": { + "name": "Schedule type", + "state": { + "frequency_reserve": "Frequency reserve", + "full_solar_export": "Full solar export", + "grid_charge": "Grid charge", + "grid_charge_discharge": "Grid charge/discharge", + "grid_discharge": "Grid discharge", + "idle": "Idle", + "inverter_charge": "Inverter charge", + "inverter_discharge": "Inverter discharge", + "solar_charge": "Solar charge", + "solar_charge_discharge": "Solar charge/discharge" + } + }, + "system_temperature": { + "name": "System temperature" + }, + "tmax": { + "name": "Maximum temperature" + }, + "tmin": { + "name": "Minimum temperature" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 28e8339630d..c95951d50bc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -298,6 +298,7 @@ FLOWS = { "homekit", "homekit_controller", "homematicip_cloud", + "homevolt", "homewizard", "homeworks", "honeywell", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index c79310e880d..d4f053e6576 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2872,6 +2872,12 @@ "zwave" ] }, + "homevolt": { + "name": "Homevolt", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "homewizard": { "name": "HomeWizard", "integration_type": "device", diff --git a/requirements_all.txt b/requirements_all.txt index 5b46238f6b1..91799717250 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1236,6 +1236,9 @@ homelink-integration-api==0.0.1 # homeassistant.components.homematicip_cloud homematicip==2.6.0 +# homeassistant.components.homevolt +homevolt==0.4.3 + # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 540b63442f4..ae88f9705d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1094,6 +1094,9 @@ homelink-integration-api==0.0.1 # homeassistant.components.homematicip_cloud homematicip==2.6.0 +# homeassistant.components.homevolt +homevolt==0.4.3 + # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homevolt/__init__.py b/tests/components/homevolt/__init__.py new file mode 100644 index 00000000000..bbeb3b3a8f2 --- /dev/null +++ b/tests/components/homevolt/__init__.py @@ -0,0 +1 @@ +"""Tests for the Homevolt integration.""" diff --git a/tests/components/homevolt/conftest.py b/tests/components/homevolt/conftest.py new file mode 100644 index 00000000000..91bf7167ca3 --- /dev/null +++ b/tests/components/homevolt/conftest.py @@ -0,0 +1,109 @@ +"""Common fixtures for the Homevolt tests.""" + +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from homevolt import DeviceMetadata, Sensor +import pytest + +from homeassistant.components.homevolt.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + +DEVICE_IDENTIFIER = "ems_40580137858664" + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.homevolt.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Homevolt", + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "test-password", + }, + unique_id="40580137858664", + ) + + +@pytest.fixture +def mock_homevolt_client() -> Generator[MagicMock]: + """Return a mocked Homevolt client.""" + with ( + patch( + "homeassistant.components.homevolt.Homevolt", + autospec=True, + ) as homevolt_mock, + patch( + "homeassistant.components.homevolt.config_flow.Homevolt", + new=homevolt_mock, + ), + ): + client = homevolt_mock.return_value + client.base_url = "http://127.0.0.1" + client.update_info = AsyncMock() + client.close_connection = AsyncMock() + + client.unique_id = "40580137858664" + + # Load sensor data from fixture and convert to Sensor objects + sensors_data = json.loads(load_fixture("sensors.json", DOMAIN)) + client.sensors = { + key: Sensor( + value=value, + type=key, + device_identifier=DEVICE_IDENTIFIER, + ) + for key, value in sensors_data.items() + } + + # Load device metadata from fixture and convert to DeviceMetadata objects + metadata_data = json.loads(load_fixture("device_metadata.json", DOMAIN)) + client.device_metadata = { + key: DeviceMetadata( + name=metadata["name"], + model=metadata["model"], + ) + for key, metadata in metadata_data.items() + } + + # Load schedule data from fixture + client.current_schedule = json.loads(load_fixture("schedule.json", DOMAIN)) + + yield client + + +@pytest.fixture +def platforms() -> list[Platform]: + """Return the platforms to test.""" + return [Platform.SENSOR] + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homevolt_client: MagicMock, + platforms: list[Platform], +) -> MockConfigEntry: + """Set up the Homevolt integration for testing.""" + mock_config_entry.add_to_hass(hass) + + with patch("homeassistant.components.homevolt.PLATFORMS", platforms): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/homevolt/fixtures/device_metadata.json b/tests/components/homevolt/fixtures/device_metadata.json new file mode 100644 index 00000000000..5bec2809ae0 --- /dev/null +++ b/tests/components/homevolt/fixtures/device_metadata.json @@ -0,0 +1,6 @@ +{ + "ems_40580137858664": { + "name": "Homevolt EMS", + "model": "EMS-1000" + } +} diff --git a/tests/components/homevolt/fixtures/schedule.json b/tests/components/homevolt/fixtures/schedule.json new file mode 100644 index 00000000000..7c4c6d46c92 --- /dev/null +++ b/tests/components/homevolt/fixtures/schedule.json @@ -0,0 +1,15 @@ +{ + "local_mode": true, + "schedule": [ + { + "type": 1, + "params": { + "setpoint": 0, + "max_charge": 6028, + "max_discharge": 6028, + "min_soc": 10, + "max_soc": 95 + } + } + ] +} diff --git a/tests/components/homevolt/fixtures/sensors.json b/tests/components/homevolt/fixtures/sensors.json new file mode 100644 index 00000000000..7692875a80c --- /dev/null +++ b/tests/components/homevolt/fixtures/sensors.json @@ -0,0 +1,37 @@ +{ + "power": -17, + "energy_imported": 308147, + "energy_exported": 272392, + "imported_energy": 106430, + "exported_energy": 143130, + "battery_state_of_charge": 91.7, + "state_of_charge": 91.7, + "charge_cycles": 21, + "l1_voltage": 234.0, + "l2_voltage": 235.1, + "l3_voltage": 232.6, + "l1_l2_voltage": 406.7, + "l2_l3_voltage": 405.1, + "l3_l1_voltage": 403.7, + "l1_current": 0.0, + "l2_current": 0.0, + "l3_current": 0.0, + "l1_power": 0.0, + "l2_power": 0.0, + "l3_power": 0.0, + "system_temperature": 9.3, + "tmin": 9.7, + "tmax": 18.8, + "frequency": 50.043, + "available_charging_power": 6028, + "available_discharge_power": 6028, + "available_charging_energy": 998, + "available_discharge_energy": 11216, + "schedule_id": "schedule_1", + "schedule_type": "idle", + "schedule_power_setpoint": 0, + "schedule_max_power": 6028, + "schedule_max_discharge": 6028, + "rssi": -84.0, + "average_rssi": -84.13 +} diff --git a/tests/components/homevolt/snapshots/test_sensor.ambr b/tests/components/homevolt/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..75d36c1e724 --- /dev/null +++ b/tests/components/homevolt/snapshots/test_sensor.ambr @@ -0,0 +1,1936 @@ +# serializer version: 1 +# name: test_entities[sensor.homevolt_ems-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '40580137858664_charge_cycles', + 'unit_of_measurement': 'cycles', + }) +# --- +# name: test_entities[sensor.homevolt_ems-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Homevolt EMS', + 'state_class': , + 'unit_of_measurement': 'cycles', + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21', + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_charging_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_available_charging_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Available charging energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Available charging energy', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'available_charging_energy', + 'unique_id': '40580137858664_available_charging_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_charging_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Homevolt EMS Available charging energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_available_charging_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '998', + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_charging_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_available_charging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Available charging power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Available charging power', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'available_charging_power', + 'unique_id': '40580137858664_available_charging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_charging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS Available charging power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_available_charging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6028', + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_discharge_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_available_discharge_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Available discharge energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Available discharge energy', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'available_discharge_energy', + 'unique_id': '40580137858664_available_discharge_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_discharge_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Homevolt EMS Available discharge energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_available_discharge_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11216', + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_discharge_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_available_discharge_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Available discharge power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Available discharge power', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'available_discharge_power', + 'unique_id': '40580137858664_available_discharge_power', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_available_discharge_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS Available discharge power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_available_discharge_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6028', + }) +# --- +# name: test_entities[sensor.homevolt_ems_average_rssi-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_average_rssi', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Average RSSI', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Average RSSI', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'average_rssi', + 'unique_id': '40580137858664_average_rssi', + 'unit_of_measurement': 'dB', + }) +# --- +# name: test_entities[sensor.homevolt_ems_average_rssi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Homevolt EMS Average RSSI', + 'state_class': , + 'unit_of_measurement': 'dB', + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_average_rssi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-84.13', + }) +# --- +# name: test_entities[sensor.homevolt_ems_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '40580137858664_state_of_charge', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.homevolt_ems_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Homevolt EMS Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '91.7', + }) +# --- +# name: test_entities[sensor.homevolt_ems_energy_exported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_energy_exported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy exported', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy exported', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_exported', + 'unique_id': '40580137858664_energy_exported', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_energy_exported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Homevolt EMS Energy exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_energy_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '272392', + }) +# --- +# name: test_entities[sensor.homevolt_ems_energy_imported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_energy_imported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy imported', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy imported', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_imported', + 'unique_id': '40580137858664_energy_imported', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_energy_imported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Homevolt EMS Energy imported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_energy_imported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '308147', + }) +# --- +# name: test_entities[sensor.homevolt_ems_exported_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_exported_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Exported energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Exported energy', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'exported_energy', + 'unique_id': '40580137858664_exported_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_exported_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Homevolt EMS Exported energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_exported_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '143130', + }) +# --- +# name: test_entities[sensor.homevolt_ems_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Frequency', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Frequency', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '40580137858664_frequency', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'Homevolt EMS Frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.043', + }) +# --- +# name: test_entities[sensor.homevolt_ems_imported_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_imported_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Imported energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Imported energy', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'imported_energy', + 'unique_id': '40580137858664_imported_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_imported_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Homevolt EMS Imported energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_imported_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '106430', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l1_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1 current', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_current', + 'unique_id': '40580137858664_l1_current', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Homevolt EMS L1 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l1_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_l2_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l1_l2_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1-L2 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1-L2 voltage', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_l2_voltage', + 'unique_id': '40580137858664_l1_l2_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_l2_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Homevolt EMS L1-L2 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l1_l2_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '406.7', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l1_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1 power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1 power', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_power', + 'unique_id': '40580137858664_l1_power', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS L1 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L1 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L1 voltage', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l1_voltage', + 'unique_id': '40580137858664_l1_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l1_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Homevolt EMS L1 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '234.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l2_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L2 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L2 current', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l2_current', + 'unique_id': '40580137858664_l2_current', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Homevolt EMS L2 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l2_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_l3_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l2_l3_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L2-L3 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L2-L3 voltage', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l2_l3_voltage', + 'unique_id': '40580137858664_l2_l3_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_l3_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Homevolt EMS L2-L3 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l2_l3_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '405.1', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l2_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L2 power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L2 power', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l2_power', + 'unique_id': '40580137858664_l2_power', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS L2 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l2_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l2_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L2 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L2 voltage', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l2_voltage', + 'unique_id': '40580137858664_l2_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l2_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Homevolt EMS L2 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l2_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '235.1', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l3_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L3 current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L3 current', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l3_current', + 'unique_id': '40580137858664_l3_current', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Homevolt EMS L3 current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l3_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_l1_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l3_l1_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L3-L1 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L3-L1 voltage', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l3_l1_voltage', + 'unique_id': '40580137858664_l3_l1_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_l1_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Homevolt EMS L3-L1 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l3_l1_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '403.7', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l3_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L3 power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L3 power', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l3_power', + 'unique_id': '40580137858664_l3_power', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS L3 power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l3_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_l3_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'L3 voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'L3 voltage', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'l3_voltage', + 'unique_id': '40580137858664_l3_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_l3_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Homevolt EMS L3 voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_l3_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '232.6', + }) +# --- +# name: test_entities[sensor.homevolt_ems_maximum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_maximum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Maximum temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Maximum temperature', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tmax', + 'unique_id': '40580137858664_tmax', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_maximum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Homevolt EMS Maximum temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_maximum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18.8', + }) +# --- +# name: test_entities[sensor.homevolt_ems_minimum_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_minimum_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Minimum temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Minimum temperature', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tmin', + 'unique_id': '40580137858664_tmin', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_minimum_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Homevolt EMS Minimum temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_minimum_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.7', + }) +# --- +# name: test_entities[sensor.homevolt_ems_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.homevolt_ems_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '40580137858664_power', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-17', + }) +# --- +# name: test_entities[sensor.homevolt_ems_rssi-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_rssi', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'RSSI', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'RSSI', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'rssi', + 'unique_id': '40580137858664_rssi', + 'unit_of_measurement': 'dB', + }) +# --- +# name: test_entities[sensor.homevolt_ems_rssi-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Homevolt EMS RSSI', + 'state_class': , + 'unit_of_measurement': 'dB', + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_rssi', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-84.0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_id-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_schedule_id', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Schedule ID', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Schedule ID', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'schedule_id', + 'unique_id': '40580137858664_schedule_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_id-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Homevolt EMS Schedule ID', + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_schedule_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'schedule_1', + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_max_discharge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_schedule_max_discharge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Schedule max discharge', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Schedule max discharge', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'schedule_max_discharge', + 'unique_id': '40580137858664_schedule_max_discharge', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_max_discharge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS Schedule max discharge', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_schedule_max_discharge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6028', + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_max_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_schedule_max_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Schedule max power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Schedule max power', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'schedule_max_power', + 'unique_id': '40580137858664_schedule_max_power', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_max_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS Schedule max power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_schedule_max_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6028', + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_power_setpoint-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_schedule_power_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Schedule power setpoint', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Schedule power setpoint', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'schedule_power_setpoint', + 'unique_id': '40580137858664_schedule_power_setpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_power_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Homevolt EMS Schedule power setpoint', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_schedule_power_setpoint', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'idle', + 'inverter_charge', + 'inverter_discharge', + 'grid_charge', + 'grid_discharge', + 'grid_charge_discharge', + 'frequency_reserve', + 'solar_charge', + 'solar_charge_discharge', + 'full_solar_export', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_schedule_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Schedule type', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Schedule type', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'schedule_type', + 'unique_id': '40580137858664_schedule_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.homevolt_ems_schedule_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Homevolt EMS Schedule type', + 'options': list([ + 'idle', + 'inverter_charge', + 'inverter_discharge', + 'grid_charge', + 'grid_discharge', + 'grid_charge_discharge', + 'frequency_reserve', + 'solar_charge', + 'solar_charge_discharge', + 'full_solar_export', + ]), + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_schedule_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_entities[sensor.homevolt_ems_system_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.homevolt_ems_system_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'System temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'System temperature', + 'platform': 'homevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'system_temperature', + 'unique_id': '40580137858664_system_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.homevolt_ems_system_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Homevolt EMS System temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.homevolt_ems_system_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.3', + }) +# --- diff --git a/tests/components/homevolt/test_config_flow.py b/tests/components/homevolt/test_config_flow.py new file mode 100644 index 00000000000..3f7d893c8f9 --- /dev/null +++ b/tests/components/homevolt/test_config_flow.py @@ -0,0 +1,225 @@ +"""Tests for the Homevolt config flow.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock + +from homevolt import HomevoltAuthenticationError, HomevoltConnectionError +import pytest + +from homeassistant.components.homevolt.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow_success( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_homevolt_client: MagicMock +) -> None: + """Test a complete successful user flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + user_input = { + CONF_HOST: "192.168.1.100", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Homevolt" + assert result["data"] == {CONF_HOST: "192.168.1.100", CONF_PASSWORD: None} + assert result["result"].unique_id == "40580137858664" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_auth_error_then_password_success( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_homevolt_client: MagicMock +) -> None: + """Test flow when authentication is required.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + user_input = { + CONF_HOST: "192.168.1.100", + } + + mock_homevolt_client.update_info.side_effect = HomevoltAuthenticationError + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "credentials" + assert result["errors"] == {} + + # Now provide password - should succeed + mock_homevolt_client.update_info.side_effect = None + + password_input = { + CONF_PASSWORD: "test-password", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], password_input + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Homevolt" + assert result["data"] == { + CONF_HOST: "192.168.1.100", + CONF_PASSWORD: "test-password", + } + assert result["result"].unique_id == "40580137858664" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (HomevoltConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_step_user_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_homevolt_client: MagicMock, + exception: Exception, + expected_error: str, +) -> None: + """Test error cases for the user step with recovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + user_input = { + CONF_HOST: "192.168.1.100", + } + + mock_homevolt_client.update_info.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": expected_error} + + mock_homevolt_client.update_info.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Homevolt" + assert result["data"] == {CONF_HOST: "192.168.1.100", CONF_PASSWORD: None} + assert result["result"].unique_id == "40580137858664" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_homevolt_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that a duplicate device_id aborts the flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + user_input = { + CONF_HOST: "192.168.1.200", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_credentials_step_invalid_password( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_homevolt_client: MagicMock +) -> None: + """Test invalid password in credentials step shows error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + user_input = { + CONF_HOST: "192.168.1.100", + } + + mock_homevolt_client.update_info.side_effect = HomevoltAuthenticationError + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "credentials" + + # Provide wrong password - should show error + password_input = { + CONF_PASSWORD: "wrong-password", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], password_input + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "credentials" + assert result["errors"] == {"base": "invalid_auth"} + + mock_homevolt_client.update_info.side_effect = None + + password_input = { + CONF_PASSWORD: "correct-password", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], password_input + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Homevolt" + assert result["data"] == { + CONF_HOST: "192.168.1.100", + CONF_PASSWORD: "correct-password", + } + assert result["result"].unique_id == "40580137858664" + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/homevolt/test_init.py b/tests/components/homevolt/test_init.py new file mode 100644 index 00000000000..9ead5839c2f --- /dev/null +++ b/tests/components/homevolt/test_init.py @@ -0,0 +1,60 @@ +"""Test the Homevolt init module.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from homevolt import HomevoltAuthenticationError, HomevoltConnectionError +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_homevolt_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + mock_homevolt_client.close_connection.assert_called_once() + + +@pytest.mark.parametrize( + ("side_effect", "expected_state"), + [ + ( + HomevoltConnectionError("Connection failed"), + ConfigEntryState.SETUP_RETRY, + ), + ( + HomevoltAuthenticationError("Authentication failed"), + ConfigEntryState.SETUP_ERROR, + ), + ], +) +async def test_config_entry_setup_failure( + hass: HomeAssistant, + mock_homevolt_client: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + expected_state: ConfigEntryState, +) -> None: + """Test the Homevolt configuration entry setup failures.""" + mock_homevolt_client.update_info.side_effect = side_effect + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_config_entry.state is expected_state diff --git a/tests/components/homevolt/test_sensor.py b/tests/components/homevolt/test_sensor.py new file mode 100644 index 00000000000..aeb28d7f738 --- /dev/null +++ b/tests/components/homevolt/test_sensor.py @@ -0,0 +1,62 @@ +"""Tests for the Homevolt sensor platform.""" + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.homevolt.const import DOMAIN, SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +pytestmark = pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "init_integration" +) + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the sensor entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "40580137858664_ems_40580137858664")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id + + +async def test_sensor_exposes_values_from_coordinator( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_homevolt_client, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure sensor entities are created and expose values from the coordinator.""" + unique_id = "40580137858664_l1_voltage" + entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id) + assert entity_id is not None + + state = hass.states.get(entity_id) + assert state is not None + assert float(state.state) == 234.0 + + mock_homevolt_client.sensors["l1_voltage"].value = 240.1 + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state is not None + assert float(state.state) == 240.1