mirror of
https://github.com/home-assistant/core.git
synced 2025-06-19 20:47:06 +00:00
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:
parent
87e5b024c1
commit
b51bb668c6
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -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
|
||||
|
31
homeassistant/components/imeon_inverter/__init__.py
Normal file
31
homeassistant/components/imeon_inverter/__init__.py
Normal 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)
|
114
homeassistant/components/imeon_inverter/config_flow.py
Normal file
114
homeassistant/components/imeon_inverter/config_flow.py
Normal 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()
|
9
homeassistant/components/imeon_inverter/const.py
Normal file
9
homeassistant/components/imeon_inverter/const.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""Constant for Imeon component."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "imeon_inverter"
|
||||
TIMEOUT = 20
|
||||
PLATFORMS = [
|
||||
Platform.SENSOR,
|
||||
]
|
97
homeassistant/components/imeon_inverter/coordinator.py
Normal file
97
homeassistant/components/imeon_inverter/coordinator.py
Normal 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
|
159
homeassistant/components/imeon_inverter/icons.json
Normal file
159
homeassistant/components/imeon_inverter/icons.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
homeassistant/components/imeon_inverter/manifest.json
Normal file
18
homeassistant/components/imeon_inverter/manifest.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
71
homeassistant/components/imeon_inverter/quality_scale.yaml
Normal file
71
homeassistant/components/imeon_inverter/quality_scale.yaml
Normal 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
|
464
homeassistant/components/imeon_inverter/sensor.py
Normal file
464
homeassistant/components/imeon_inverter/sensor.py
Normal 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)
|
187
homeassistant/components/imeon_inverter/strings.json
Normal file
187
homeassistant/components/imeon_inverter/strings.json
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -285,6 +285,7 @@ FLOWS = {
|
||||
"ifttt",
|
||||
"igloohome",
|
||||
"imap",
|
||||
"imeon_inverter",
|
||||
"imgw_pib",
|
||||
"improv_ble",
|
||||
"incomfort",
|
||||
|
@ -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",
|
||||
|
7
homeassistant/generated/ssdp.py
generated
7
homeassistant/generated/ssdp.py
generated
@ -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
3
requirements_all.txt
generated
@ -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
|
||||
|
||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -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
|
||||
|
||||
|
14
tests/components/imeon_inverter/__init__.py
Normal file
14
tests/components/imeon_inverter/__init__.py
Normal 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()
|
85
tests/components/imeon_inverter/conftest.py
Normal file
85
tests/components/imeon_inverter/conftest.py
Normal 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,
|
||||
)
|
73
tests/components/imeon_inverter/fixtures/sensor_data.json
Normal file
73
tests/components/imeon_inverter/fixtures/sensor_data.json
Normal 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
|
||||
}
|
||||
}
|
2689
tests/components/imeon_inverter/snapshots/test_sensor.ambr
Normal file
2689
tests/components/imeon_inverter/snapshots/test_sensor.ambr
Normal file
File diff suppressed because it is too large
Load Diff
205
tests/components/imeon_inverter/test_config_flow.py
Normal file
205
tests/components/imeon_inverter/test_config_flow.py
Normal 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"
|
29
tests/components/imeon_inverter/test_sensor.py
Normal file
29
tests/components/imeon_inverter/test_sensor.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user