Add imeon inverter integration (#130958)

* Initial commit prototype with empty inverters

* Use modern methods and global variable for character strings

* Platform that get the value of the meter in an entity

* Add check if inverter already configured

* Add tests for config_flow

* Update "imeon_inverter_api" in manifest.json

* Update "imeon_inverter_api" in requirements_all.txt

* Remove async_setup, clean comments, use of const PLATFORM

* Use of global variable and remove configuration of device name

* Use of entry.data instead of user_input variable

* Remove services.yaml

* No quality scale

* Use of common string

* Add sensors, use of EntityDescription and '_attr_device_info'

* Remove name from config_flow tests

* Use sentence case and change integration from hub to device

* Check connection before add platform in config_flow

* Use of _async_setup and minor changes

* Improve sensor description

* Add quality_scale.yaml

* Update the quality_scale.json

* Add tests for host invalid, route invalid, exception and invalid auth

* Type more precisely 'DataUpdateCoordinator'

* Don't use 'self.data' directly in coordinator and minor corrections

* Complete full quality_scale.yaml

* Use of fixtures in the tests

* Add snapshot tests for sensors

* Refactor the try except and use serial as unique id

* Change API version

* Add test for sensor

* Mock the api to generate the snapshot

* New type for async_add_entries

* Except timeout error for get_serial

* Add test for get_serial timeout error

* Move store data out of the try

* Use sentence case

* Use of fixtures

* Use separates fixtures

* Mock the api

* Put sensors fake data in json fixture file

* Use of a const interval, remove except timeout, enhance lisibility

* Try to use same fixture in test_config_flow

* Try use same fixture for all mock of inverter

* Modify the fixture in the context manager, correct the tests

* Fixture return mock.__aenter__ directly

* Adjust code clarity

* Bring all tests to either ABORT or CREATE_ENTRY

* Make the try except more concise

* Synthetize exception tests into one

* Add code clarity

* Nitpick with the tests

* Use unique id sensor

* Log an error on unknown error

* Remove useless comments, disable always_update and better use of timeout

* Adjust units, set the model and software version

* Set full name for Battery SOC and use ip instead of url

* Use of host instead of IP

* Fix the unit of economy factor

* Reduce mornitoring data display precision and update snapshots

* Remove unused variable HUBs

* Fix device info

* Set address label 'Host or IP'

* Fix the config_flow tests

* Re evaluate the quality_scale

* Use of 'host' instead of 'address'

* Make inverter discoverable by ssdp

* Add test ssdp configuration already exist

* Add exemption in quality scale

* Test abort ssdp if serial is unknown

* Handle update error

* Raise other exceptions

* Handle ClientError and ValueError from the api

* Update homeassistant/components/imeon_inverter/quality_scale.yaml

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
Imeon-Energy 2025-04-10 08:25:35 +02:00 committed by GitHub
parent 87e5b024c1
commit b51bb668c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 4267 additions and 0 deletions

2
CODEOWNERS generated
View File

@ -704,6 +704,8 @@ build.json @home-assistant/supervisor
/tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh
/homeassistant/components/imeon_inverter/ @Imeon-Energy
/tests/components/imeon_inverter/ @Imeon-Energy
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/improv_ble/ @emontnemery

View File

@ -0,0 +1,31 @@
"""Initialize the Imeon component."""
from __future__ import annotations
import logging
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
from .coordinator import InverterConfigEntry, InverterCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: InverterConfigEntry) -> bool:
"""Handle the creation of a new config entry for the integration (asynchronous)."""
# Create the corresponding HUB
coordinator = InverterCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
# Call for HUB creation then each entity as a List
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: InverterConfigEntry) -> bool:
"""Handle entry unloading."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,114 @@
"""Config flow for Imeon integration."""
import logging
from typing import Any
from urllib.parse import urlparse
from imeon_inverter_api.inverter import Inverter
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.service_info.ssdp import (
ATTR_UPNP_MODEL_NUMBER,
ATTR_UPNP_SERIAL,
SsdpServiceInfo,
)
from homeassistant.helpers.typing import VolDictType
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class ImeonInverterConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial setup flow for Imeon Inverters."""
_host: str | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step for creating a new configuration entry."""
errors: dict[str, str] = {}
if user_input is not None:
# User have to provide the hostname if device is not discovered
host = self._host or user_input[CONF_HOST]
async with Inverter(host) as client:
try:
# Check connection
if await client.login(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
):
serial = await client.get_serial()
else:
errors["base"] = "invalid_auth"
except TimeoutError:
errors["base"] = "cannot_connect"
except ValueError as e:
if "Host invalid" in str(e):
errors["base"] = "invalid_host"
elif "Route invalid" in str(e):
errors["base"] = "invalid_route"
else:
errors["base"] = "unknown"
_LOGGER.exception(
"Unexpected error occurred while connecting to the Imeon"
)
if not errors:
# Check if entry already exists
await self.async_set_unique_id(serial, raise_on_progress=False)
self._abort_if_unique_id_configured()
# Create a new configuration entry if login succeeds
return self.async_create_entry(
title=f"Imeon {serial}", data={CONF_HOST: host, **user_input}
)
host_schema: VolDictType = (
{vol.Required(CONF_HOST): str} if not self._host else {}
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
**host_schema,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
)
async def async_step_ssdp(
self, discovery_info: SsdpServiceInfo
) -> ConfigFlowResult:
"""Handle a SSDP discovery."""
host = str(urlparse(discovery_info.ssdp_location).hostname)
serial = discovery_info.upnp.get(ATTR_UPNP_SERIAL, "")
if not serial:
return self.async_abort(reason="cannot_connect")
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._host = host
self.context["title_placeholders"] = {
"model": discovery_info.upnp.get(ATTR_UPNP_MODEL_NUMBER, ""),
"serial": serial,
}
return await self.async_step_user()

View File

@ -0,0 +1,9 @@
"""Constant for Imeon component."""
from homeassistant.const import Platform
DOMAIN = "imeon_inverter"
TIMEOUT = 20
PLATFORMS = [
Platform.SENSOR,
]

View File

@ -0,0 +1,97 @@
"""Coordinator for Imeon integration."""
from __future__ import annotations
from asyncio import timeout
from datetime import timedelta
import logging
from aiohttp import ClientError
from imeon_inverter_api.inverter import Inverter
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import TIMEOUT
HUBNAME = "imeon_inverter_hub"
INTERVAL = timedelta(seconds=60)
_LOGGER = logging.getLogger(__name__)
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
# HUB CREATION #
class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
"""Each inverter is it's own HUB, thus it's own data set.
This allows this integration to handle as many
inverters as possible in parallel.
"""
config_entry: InverterConfigEntry
# Implement methods to fetch and update data
def __init__(
self,
hass: HomeAssistant,
entry: InverterConfigEntry,
) -> None:
"""Initialize data update coordinator."""
super().__init__(
hass,
_LOGGER,
name=HUBNAME,
update_interval=INTERVAL,
config_entry=entry,
)
self._api = Inverter(entry.data[CONF_HOST])
@property
def api(self) -> Inverter:
"""Return the inverter object."""
return self._api
async def _async_setup(self) -> None:
"""Set up the coordinator."""
async with timeout(TIMEOUT):
await self._api.login(
self.config_entry.data[CONF_USERNAME],
self.config_entry.data[CONF_PASSWORD],
)
await self._api.init()
async def _async_update_data(self) -> dict[str, str | float | int]:
"""Fetch and store newest data from API.
This is the place to where entities can get their data.
It also includes the login process.
"""
data: dict[str, str | float | int] = {}
async with timeout(TIMEOUT):
await self._api.login(
self.config_entry.data[CONF_USERNAME],
self.config_entry.data[CONF_PASSWORD],
)
# Fetch data using distant API
try:
await self._api.update()
except (ValueError, ClientError) as e:
raise UpdateFailed(e) from e
# Store data
for key, val in self._api.storage.items():
if key == "timeline":
data[key] = val
else:
for sub_key, sub_val in val.items():
data[f"{key}_{sub_key}"] = sub_val
return data

View File

@ -0,0 +1,159 @@
{
"entity": {
"sensor": {
"battery_autonomy": {
"default": "mdi:battery-clock"
},
"battery_charge_time": {
"default": "mdi:battery-charging"
},
"battery_power": {
"default": "mdi:battery"
},
"battery_soc": {
"default": "mdi:battery-charging-100"
},
"battery_stored": {
"default": "mdi:battery"
},
"grid_current_l1": {
"default": "mdi:current-ac"
},
"grid_current_l2": {
"default": "mdi:current-ac"
},
"grid_current_l3": {
"default": "mdi:current-ac"
},
"grid_frequency": {
"default": "mdi:sine-wave"
},
"grid_voltage_l1": {
"default": "mdi:flash"
},
"grid_voltage_l2": {
"default": "mdi:flash"
},
"grid_voltage_l3": {
"default": "mdi:flash"
},
"input_power_l1": {
"default": "mdi:power-socket"
},
"input_power_l2": {
"default": "mdi:power-socket"
},
"input_power_l3": {
"default": "mdi:power-socket"
},
"input_power_total": {
"default": "mdi:power-plug"
},
"inverter_charging_current_limit": {
"default": "mdi:current-dc"
},
"inverter_injection_power_limit": {
"default": "mdi:power-socket"
},
"meter_power": {
"default": "mdi:power-plug"
},
"meter_power_protocol": {
"default": "mdi:protocol"
},
"output_current_l1": {
"default": "mdi:current-ac"
},
"output_current_l2": {
"default": "mdi:current-ac"
},
"output_current_l3": {
"default": "mdi:current-ac"
},
"output_frequency": {
"default": "mdi:sine-wave"
},
"output_power_l1": {
"default": "mdi:power-socket"
},
"output_power_l2": {
"default": "mdi:power-socket"
},
"output_power_l3": {
"default": "mdi:power-socket"
},
"output_power_total": {
"default": "mdi:power-plug"
},
"output_voltage_l1": {
"default": "mdi:flash"
},
"output_voltage_l2": {
"default": "mdi:flash"
},
"output_voltage_l3": {
"default": "mdi:flash"
},
"pv_consumed": {
"default": "mdi:solar-power"
},
"pv_injected": {
"default": "mdi:solar-power"
},
"pv_power_1": {
"default": "mdi:solar-power"
},
"pv_power_2": {
"default": "mdi:solar-power"
},
"pv_power_total": {
"default": "mdi:solar-power"
},
"temp_air_temperature": {
"default": "mdi:thermometer"
},
"temp_component_temperature": {
"default": "mdi:thermometer"
},
"monitoring_building_consumption": {
"default": "mdi:home-lightning-bolt"
},
"monitoring_economy_factor": {
"default": "mdi:chart-bar"
},
"monitoring_grid_consumption": {
"default": "mdi:transmission-tower"
},
"monitoring_grid_injection": {
"default": "mdi:transmission-tower-export"
},
"monitoring_grid_power_flow": {
"default": "mdi:power-plug"
},
"monitoring_self_consumption": {
"default": "mdi:percent"
},
"monitoring_self_sufficiency": {
"default": "mdi:percent"
},
"monitoring_solar_production": {
"default": "mdi:solar-power"
},
"monitoring_minute_building_consumption": {
"default": "mdi:home-lightning-bolt"
},
"monitoring_minute_grid_consumption": {
"default": "mdi:transmission-tower"
},
"monitoring_minute_grid_injection": {
"default": "mdi:transmission-tower-export"
},
"monitoring_minute_grid_power_flow": {
"default": "mdi:power-plug"
},
"monitoring_minute_solar_production": {
"default": "mdi:solar-power"
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"domain": "imeon_inverter",
"name": "Imeon Inverter",
"codeowners": ["@Imeon-Energy"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/imeon_inverter",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["imeon_inverter_api==0.3.12"],
"ssdp": [
{
"manufacturer": "IMEON",
"deviceType": "urn:schemas-upnp-org:device:Basic:1",
"st": "upnp:rootdevice"
}
]
}

View File

@ -0,0 +1,71 @@
rules:
# Bronze
config-flow: done
test-before-configure: done
unique-config-entry: done
config-flow-test-coverage: done
runtime-data: done
test-before-setup: done
appropriate-polling: done
entity-unique-id: done
has-entity-name: done
entity-event-setup:
status: exempt
comment: This integration doesn't have sensors that subscribe to events.
dependency-transparency: done
action-setup:
status: exempt
comment: This integration does not have any service for now.
common-modules: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-actions:
status: exempt
comment: This integration does not have any service for now.
brands: done
# Silver
action-exceptions:
status: exempt
comment: This integration does not have any service for now.
config-entry-unloading: todo
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: done
discovery: done
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:
status: exempt
comment: Device type integration.
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: Currently no issues.
stale-devices:
status: exempt
comment: Device type integration.
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo

View File

@ -0,0 +1,464 @@
"""Imeon inverter sensor support."""
import logging
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
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
from .coordinator import InverterCoordinator
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
_LOGGER = logging.getLogger(__name__)
ENTITY_DESCRIPTIONS = (
# Battery
SensorEntityDescription(
key="battery_autonomy",
translation_key="battery_autonomy",
native_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="battery_charge_time",
translation_key="battery_charge_time",
native_unit_of_measurement=UnitOfTime.HOURS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="battery_power",
translation_key="battery_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="battery_soc",
translation_key="battery_soc",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="battery_stored",
translation_key="battery_stored",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY_STORAGE,
state_class=SensorStateClass.TOTAL,
),
# Grid
SensorEntityDescription(
key="grid_current_l1",
translation_key="grid_current_l1",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="grid_current_l2",
translation_key="grid_current_l2",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="grid_current_l3",
translation_key="grid_current_l3",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="grid_frequency",
translation_key="grid_frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="grid_voltage_l1",
translation_key="grid_voltage_l1",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="grid_voltage_l2",
translation_key="grid_voltage_l2",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="grid_voltage_l3",
translation_key="grid_voltage_l3",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
# AC Input
SensorEntityDescription(
key="input_power_l1",
translation_key="input_power_l1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="input_power_l2",
translation_key="input_power_l2",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="input_power_l3",
translation_key="input_power_l3",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="input_power_total",
translation_key="input_power_total",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
# Inverter settings
SensorEntityDescription(
key="inverter_charging_current_limit",
translation_key="inverter_charging_current_limit",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="inverter_injection_power_limit",
translation_key="inverter_injection_power_limit",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
# Meter
SensorEntityDescription(
key="meter_power",
translation_key="meter_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="meter_power_protocol",
translation_key="meter_power_protocol",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
# AC Output
SensorEntityDescription(
key="output_current_l1",
translation_key="output_current_l1",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_current_l2",
translation_key="output_current_l2",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_current_l3",
translation_key="output_current_l3",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_frequency",
translation_key="output_frequency",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_power_l1",
translation_key="output_power_l1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_power_l2",
translation_key="output_power_l2",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_power_l3",
translation_key="output_power_l3",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_power_total",
translation_key="output_power_total",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_voltage_l1",
translation_key="output_voltage_l1",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_voltage_l2",
translation_key="output_voltage_l2",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="output_voltage_l3",
translation_key="output_voltage_l3",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
# Solar Panel
SensorEntityDescription(
key="pv_consumed",
translation_key="pv_consumed",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="pv_injected",
translation_key="pv_injected",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key="pv_power_1",
translation_key="pv_power_1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="pv_power_2",
translation_key="pv_power_2",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="pv_power_total",
translation_key="pv_power_total",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
# Temperature
SensorEntityDescription(
key="temp_air_temperature",
translation_key="temp_air_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="temp_component_temperature",
translation_key="temp_component_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
# Monitoring (data over the last 24 hours)
SensorEntityDescription(
key="monitoring_building_consumption",
translation_key="monitoring_building_consumption",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_economy_factor",
translation_key="monitoring_economy_factor",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_grid_consumption",
translation_key="monitoring_grid_consumption",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_grid_injection",
translation_key="monitoring_grid_injection",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_grid_power_flow",
translation_key="monitoring_grid_power_flow",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_self_consumption",
translation_key="monitoring_self_consumption",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_self_sufficiency",
translation_key="monitoring_self_sufficiency",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_solar_production",
translation_key="monitoring_solar_production",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
# Monitoring (instant minute data)
SensorEntityDescription(
key="monitoring_minute_building_consumption",
translation_key="monitoring_minute_building_consumption",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_minute_grid_consumption",
translation_key="monitoring_minute_grid_consumption",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_minute_grid_injection",
translation_key="monitoring_minute_grid_injection",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_minute_grid_power_flow",
translation_key="monitoring_minute_grid_power_flow",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_minute_solar_production",
translation_key="monitoring_minute_solar_production",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: InverterConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Create each sensor for a given config entry."""
coordinator = entry.runtime_data
# Init sensor entities
async_add_entities(
InverterSensor(coordinator, entry, description)
for description in ENTITY_DESCRIPTIONS
)
class InverterSensor(CoordinatorEntity[InverterCoordinator], SensorEntity):
"""A sensor that returns numerical values with units."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self,
coordinator: InverterCoordinator,
entry: InverterConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self.entity_description = description
self._inverter = coordinator.api.inverter
self.data_key = description.key
assert entry.unique_id
self._attr_unique_id = f"{entry.unique_id}_{self.data_key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.unique_id)},
name="Imeon inverter",
manufacturer="Imeon Energy",
model=self._inverter.get("inverter"),
sw_version=self._inverter.get("software"),
)
@property
def native_value(self) -> StateType | None:
"""Value of the sensor."""
return self.coordinator.data.get(self.data_key)

View File

@ -0,0 +1,187 @@
{
"config": {
"flow_title": "Imeon {model} ({serial})",
"step": {
"user": {
"title": "Add Imeon inverter",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP of your inverter",
"username": "The username of your OS One account",
"password": "The password of your OS One account"
}
}
},
"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%]",
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"invalid_route": "Unable to request the API, make sure 'API Module' is enabled on your device",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
"sensor": {
"battery_autonomy": {
"name": "Battery autonomy"
},
"battery_charge_time": {
"name": "Battery charge time"
},
"battery_power": {
"name": "Battery power"
},
"battery_soc": {
"name": "Battery state of charge"
},
"battery_stored": {
"name": "Battery stored"
},
"grid_current_l1": {
"name": "Grid current L1"
},
"grid_current_l2": {
"name": "Grid current L2"
},
"grid_current_l3": {
"name": "Grid current L3"
},
"grid_frequency": {
"name": "Grid frequency"
},
"grid_voltage_l1": {
"name": "Grid voltage L1"
},
"grid_voltage_l2": {
"name": "Grid voltage L2"
},
"grid_voltage_l3": {
"name": "Grid voltage L3"
},
"input_power_l1": {
"name": "Input power L1"
},
"input_power_l2": {
"name": "Input power L2"
},
"input_power_l3": {
"name": "Input power L3"
},
"input_power_total": {
"name": "Input power total"
},
"inverter_charging_current_limit": {
"name": "Charging current limit"
},
"inverter_injection_power_limit": {
"name": "Injection power limit"
},
"meter_power": {
"name": "Meter power"
},
"meter_power_protocol": {
"name": "Meter power protocol"
},
"output_current_l1": {
"name": "Output current L1"
},
"output_current_l2": {
"name": "Output current L2"
},
"output_current_l3": {
"name": "Output current L3"
},
"output_frequency": {
"name": "Output frequency"
},
"output_power_l1": {
"name": "Output power L1"
},
"output_power_l2": {
"name": "Output power L2"
},
"output_power_l3": {
"name": "Output power L3"
},
"output_power_total": {
"name": "Output power total"
},
"output_voltage_l1": {
"name": "Output voltage L1"
},
"output_voltage_l2": {
"name": "Output voltage L2"
},
"output_voltage_l3": {
"name": "Output voltage L3"
},
"pv_consumed": {
"name": "PV consumed"
},
"pv_injected": {
"name": "PV injected"
},
"pv_power_1": {
"name": "PV power 1"
},
"pv_power_2": {
"name": "PV power 2"
},
"pv_power_total": {
"name": "PV power total"
},
"temp_air_temperature": {
"name": "Air temperature"
},
"temp_component_temperature": {
"name": "Component temperature"
},
"monitoring_building_consumption": {
"name": "Monitoring building consumption"
},
"monitoring_economy_factor": {
"name": "Monitoring economy factor"
},
"monitoring_grid_consumption": {
"name": "Monitoring grid consumption"
},
"monitoring_grid_injection": {
"name": "Monitoring grid injection"
},
"monitoring_grid_power_flow": {
"name": "Monitoring grid power flow"
},
"monitoring_self_consumption": {
"name": "Monitoring self consumption"
},
"monitoring_self_sufficiency": {
"name": "Monitoring self sufficiency"
},
"monitoring_solar_production": {
"name": "Monitoring solar production"
},
"monitoring_minute_building_consumption": {
"name": "Monitoring building consumption (minute)"
},
"monitoring_minute_grid_consumption": {
"name": "Monitoring grid consumption (minute)"
},
"monitoring_minute_grid_injection": {
"name": "Monitoring grid injection (minute)"
},
"monitoring_minute_grid_power_flow": {
"name": "Monitoring grid power flow (minute)"
},
"monitoring_minute_solar_production": {
"name": "Monitoring solar production (minute)"
}
}
}
}

View File

@ -285,6 +285,7 @@ FLOWS = {
"ifttt",
"igloohome",
"imap",
"imeon_inverter",
"imgw_pib",
"improv_ble",
"incomfort",

View File

@ -2935,6 +2935,12 @@
"config_flow": true,
"iot_class": "cloud_push"
},
"imeon_inverter": {
"name": "Imeon Inverter",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"imgw_pib": {
"name": "IMGW-PIB",
"integration_type": "hub",

View File

@ -166,6 +166,13 @@ SSDP = {
"st": "urn:hyperion-project.org:device:basic:1",
},
],
"imeon_inverter": [
{
"deviceType": "urn:schemas-upnp-org:device:Basic:1",
"manufacturer": "IMEON",
"st": "upnp:rootdevice",
},
],
"isy994": [
{
"deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1",

3
requirements_all.txt generated
View File

@ -1219,6 +1219,9 @@ igloohome-api==0.1.0
# homeassistant.components.ihc
ihcsdk==2.8.5
# homeassistant.components.imeon_inverter
imeon_inverter_api==0.3.12
# homeassistant.components.imgw_pib
imgw_pib==1.0.10

View File

@ -1034,6 +1034,9 @@ ifaddr==0.2.0
# homeassistant.components.igloohome
igloohome-api==0.1.0
# homeassistant.components.imeon_inverter
imeon_inverter_api==0.3.12
# homeassistant.components.imgw_pib
imgw_pib==1.0.10

View File

@ -0,0 +1,14 @@
"""Tests for the Imeon Inverter integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Set up the Imeon Inverter integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,85 @@
"""Configuration for the Imeon Inverter integration tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock
import pytest
from homeassistant.components.imeon_inverter.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.service_info.ssdp import (
ATTR_UPNP_DEVICE_TYPE,
ATTR_UPNP_FRIENDLY_NAME,
ATTR_UPNP_MANUFACTURER,
ATTR_UPNP_MODEL_NAME,
ATTR_UPNP_SERIAL,
ATTR_UPNP_UDN,
SsdpServiceInfo,
)
from tests.common import MockConfigEntry, load_json_object_fixture, patch
# Sample test data
TEST_USER_INPUT = {
CONF_HOST: "192.168.200.1",
CONF_USERNAME: "user@local",
CONF_PASSWORD: "password",
}
TEST_SERIAL = "111111111111111"
TEST_DISCOVER = SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
ssdp_location=f"http://{TEST_USER_INPUT[CONF_HOST]}:8088/imeon.xml",
upnp={
ATTR_UPNP_MANUFACTURER: "IMEON",
ATTR_UPNP_MODEL_NAME: "IMEON",
ATTR_UPNP_FRIENDLY_NAME: f"IMEON-{TEST_SERIAL}",
ATTR_UPNP_SERIAL: TEST_SERIAL,
ATTR_UPNP_UDN: "uuid:01234567-89ab-cdef-0123-456789abcdef",
ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:Basic:1",
},
)
@pytest.fixture(autouse=True)
def mock_imeon_inverter() -> Generator[MagicMock]:
"""Mock data from the device."""
with (
patch(
"homeassistant.components.imeon_inverter.coordinator.Inverter",
autospec=True,
) as inverter_mock,
patch(
"homeassistant.components.imeon_inverter.config_flow.Inverter",
new=inverter_mock,
),
):
inverter = inverter_mock.return_value
inverter.__aenter__.return_value = inverter
inverter.login.return_value = True
inverter.get_serial.return_value = TEST_SERIAL
inverter.storage = load_json_object_fixture("sensor_data.json", DOMAIN)
yield inverter
@pytest.fixture
def mock_async_setup_entry() -> Generator[AsyncMock]:
"""Fixture for mocking async_setup_entry."""
with patch(
"homeassistant.components.imeon_inverter.async_setup_entry",
return_value=True,
) as mock:
yield mock
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
title="Imeon inverter",
domain=DOMAIN,
data=TEST_USER_INPUT,
unique_id=TEST_SERIAL,
)

View File

@ -0,0 +1,73 @@
{
"battery": {
"autonomy": 4.5,
"charge_time": 120,
"power": 2500.0,
"soc": 78.0,
"stored": 10.2
},
"grid": {
"current_l1": 12.5,
"current_l2": 10.8,
"current_l3": 11.2,
"frequency": 50.0,
"voltage_l1": 230.0,
"voltage_l2": 229.5,
"voltage_l3": 230.1
},
"input": {
"power_l1": 1000.0,
"power_l2": 950.0,
"power_l3": 980.0,
"power_total": 2930.0
},
"inverter": {
"charging_current_limit": 50,
"injection_power_limit": 5000.0
},
"meter": {
"power": 2000.0,
"power_protocol": 2018.0
},
"output": {
"current_l1": 15.0,
"current_l2": 14.5,
"current_l3": 15.2,
"frequency": 49.9,
"power_l1": 1100.0,
"power_l2": 1080.0,
"power_l3": 1120.0,
"power_total": 3300.0,
"voltage_l1": 231.0,
"voltage_l2": 229.8,
"voltage_l3": 230.2
},
"pv": {
"consumed": 1500.0,
"injected": 800.0,
"power_1": 1200.0,
"power_2": 1300.0,
"power_total": 2500.0
},
"temp": {
"air_temperature": 25.0,
"component_temperature": 45.5
},
"monitoring": {
"building_consumption": 3000.0,
"economy_factor": 0.8,
"grid_consumption": 500.0,
"grid_injection": 700.0,
"grid_power_flow": -200.0,
"self_consumption": 85.0,
"self_sufficiency": 90.0,
"solar_production": 2600.0
},
"monitoring_minute": {
"building_consumption": 50.0,
"grid_consumption": 8.3,
"grid_injection": 11.7,
"grid_power_flow": -3.4,
"solar_production": 43.3
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,205 @@
"""Test the Imeon Inverter config flow."""
from copy import deepcopy
from unittest.mock import AsyncMock, MagicMock
import pytest
from homeassistant.components.imeon_inverter.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_SOURCE
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_SERIAL
from .conftest import TEST_DISCOVER, TEST_SERIAL, TEST_USER_INPUT
from tests.common import MockConfigEntry
pytestmark = pytest.mark.usefixtures("mock_async_setup_entry")
async def test_form_valid(
hass: HomeAssistant,
mock_async_setup_entry: AsyncMock,
) -> None:
"""Test we get the form and the config is created with the good entries."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Imeon {TEST_SERIAL}"
assert result["data"] == TEST_USER_INPUT
assert result["result"].unique_id == TEST_SERIAL
assert mock_async_setup_entry.call_count == 1
async def test_form_invalid_auth(
hass: HomeAssistant, mock_imeon_inverter: MagicMock
) -> None:
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
mock_imeon_inverter.login.return_value = False
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "invalid_auth"}
mock_imeon_inverter.login.return_value = True
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@pytest.mark.parametrize(
("error", "expected"),
[
(TimeoutError, "cannot_connect"),
(ValueError("Host invalid"), "invalid_host"),
(ValueError("Route invalid"), "invalid_route"),
(ValueError, "unknown"),
],
)
async def test_form_exception(
hass: HomeAssistant,
mock_imeon_inverter: MagicMock,
error: Exception,
expected: str,
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
mock_imeon_inverter.login.side_effect = error
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": expected}
mock_imeon_inverter.login.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_manual_setup_already_exists(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a flow with an existing id aborts."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_get_serial_timeout(
hass: HomeAssistant, mock_imeon_inverter: MagicMock
) -> None:
"""Test the timeout error handling of getting the serial number."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
mock_imeon_inverter.get_serial.side_effect = TimeoutError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
mock_imeon_inverter.get_serial.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_USER_INPUT
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_ssdp(hass: HomeAssistant) -> None:
"""Test a ssdp discovery."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data=TEST_DISCOVER,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
user_input = TEST_USER_INPUT.copy()
user_input.pop(CONF_HOST)
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == f"Imeon {TEST_SERIAL}"
assert result["data"] == TEST_USER_INPUT
async def test_ssdp_already_exist(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a ssdp discovery flow with an existing id aborts."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data=TEST_DISCOVER,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_ssdp_abort(hass: HomeAssistant) -> None:
"""Test that a ssdp discovery aborts if serial is unknown."""
data = deepcopy(TEST_DISCOVER)
data.upnp.pop(ATTR_UPNP_SERIAL, None)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data=data,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"

View File

@ -0,0 +1,29 @@
"""Test the Imeon Inverter sensors."""
from unittest.mock import MagicMock, patch
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_sensors(
hass: HomeAssistant,
mock_imeon_inverter: MagicMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the Imeon Inverter sensors."""
with patch(
"homeassistant.components.imeon_inverter.const.PLATFORMS", [Platform.SENSOR]
):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)