Add sensors to Imeon inverter integration (#146437)

Co-authored-by: TheBushBoy <theodavid@icloud.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Imeon-Energy
2025-08-19 14:21:33 +02:00
committed by GitHub
parent e6a158b1ac
commit 4c1788e757
10 changed files with 1370 additions and 515 deletions

View File

@@ -7,3 +7,26 @@ TIMEOUT = 30
PLATFORMS = [
Platform.SENSOR,
]
ATTR_BATTERY_STATUS = ["charging", "discharging", "charged"]
ATTR_INVERTER_STATE = [
"unsynchronized",
"grid_consumption",
"grid_injection",
"grid_synchronised_but_not_used",
]
ATTR_TIMELINE_STATUS = [
"com_lost",
"warning_grid",
"warning_pv",
"warning_bat",
"error_ond",
"error_soft",
"error_pv",
"error_grid",
"error_bat",
"good_1",
"info_soft",
"info_ond",
"info_bat",
"info_smartlo",
]

View File

@@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import TIMEOUT
HUBNAME = "imeon_inverter_hub"
INTERVAL = timedelta(seconds=60)
INTERVAL = 60
_LOGGER = logging.getLogger(__name__)
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
@@ -44,7 +44,7 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
hass,
_LOGGER,
name=HUBNAME,
update_interval=INTERVAL,
update_interval=timedelta(seconds=INTERVAL),
config_entry=entry,
)
@@ -83,7 +83,7 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
# Fetch data using distant API
try:
await self._api.update()
except (ValueError, ClientError) as e:
except (ValueError, TimeoutError, ClientError) as e:
raise UpdateFailed(e) from e
# Store data

View File

@@ -7,8 +7,14 @@
"battery_soc": {
"default": "mdi:battery-charging-100"
},
"battery_status": {
"default": "mdi:battery-alert"
},
"battery_stored": {
"default": "mdi:battery"
"default": "mdi:battery-arrow-up"
},
"battery_consumed": {
"default": "mdi:battery-arrow-down"
},
"grid_current_l1": {
"default": "mdi:current-ac"
@@ -50,7 +56,7 @@
"default": "mdi:power-socket"
},
"meter_power": {
"default": "mdi:power-plug"
"default": "mdi:meter-electric"
},
"output_current_l1": {
"default": "mdi:current-ac"
@@ -116,7 +122,7 @@
"default": "mdi:home-lightning-bolt"
},
"monitoring_minute_grid_consumption": {
"default": "mdi:transmission-tower"
"default": "mdi:transmission-tower-import"
},
"monitoring_minute_grid_injection": {
"default": "mdi:transmission-tower-export"
@@ -126,6 +132,43 @@
},
"monitoring_minute_solar_production": {
"default": "mdi:solar-power"
},
"timeline_type_msg": {
"default": "mdi:check-circle",
"state": {
"com_lost": "mdi:lan-disconnect",
"warning_grid": "mdi:alert-circle",
"warning_pv": "mdi:alert-circle",
"warning_bat": "mdi:alert-circle",
"error_ond": "mdi:close-octagon",
"error_soft": "mdi:close-octagon",
"error_pv": "mdi:close-octagon",
"error_grid": "mdi:close-octagon",
"error_bat": "mdi:close-octagon",
"good_1": "mdi:check-circle",
"info_soft": "mdi:information-slab-circle",
"info_ond": "mdi:information-slab-circle",
"info_bat": "mdi:information-slab-circle",
"info_smartlo": "mdi:information-slab-circle"
}
},
"energy_pv": {
"default": "mdi:solar-power"
},
"energy_grid_injected": {
"default": "mdi:transmission-tower-export"
},
"energy_grid_consumed": {
"default": "mdi:transmission-tower-import"
},
"energy_building_consumption": {
"default": "mdi:home-lightning-bolt-outline"
},
"energy_battery_stored": {
"default": "mdi:battery-arrow-up-outline"
},
"energy_battery_consumed": {
"default": "mdi:battery-arrow-down-outline"
}
}
}

View File

@@ -14,6 +14,7 @@ from homeassistant.const import (
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
@@ -22,6 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import ATTR_BATTERY_STATUS, ATTR_INVERTER_STATE, ATTR_TIMELINE_STATUS
from .coordinator import InverterCoordinator
from .entity import InverterEntity
@@ -46,6 +48,12 @@ SENSOR_DESCRIPTIONS = (
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="battery_status",
translation_key="battery_status",
device_class=SensorDeviceClass.ENUM,
options=ATTR_BATTERY_STATUS,
),
SensorEntityDescription(
key="battery_stored",
translation_key="battery_stored",
@@ -53,6 +61,13 @@ SENSOR_DESCRIPTIONS = (
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="battery_consumed",
translation_key="battery_consumed",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
# Grid
SensorEntityDescription(
key="grid_current_l1",
@@ -147,6 +162,12 @@ SENSOR_DESCRIPTIONS = (
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="manager_inverter_state",
translation_key="manager_inverter_state",
device_class=SensorDeviceClass.ENUM,
options=ATTR_INVERTER_STATE,
),
# Meter
SensorEntityDescription(
key="meter_power",
@@ -340,6 +361,62 @@ SENSOR_DESCRIPTIONS = (
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
),
# Timeline
SensorEntityDescription(
key="timeline_type_msg",
translation_key="timeline_type_msg",
device_class=SensorDeviceClass.ENUM,
options=ATTR_TIMELINE_STATUS,
),
# Daily energy counters
SensorEntityDescription(
key="energy_pv",
translation_key="energy_pv",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
SensorEntityDescription(
key="energy_grid_injected",
translation_key="energy_grid_injected",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
SensorEntityDescription(
key="energy_grid_consumed",
translation_key="energy_grid_consumed",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
SensorEntityDescription(
key="energy_building_consumption",
translation_key="energy_building_consumption",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
SensorEntityDescription(
key="energy_battery_stored",
translation_key="energy_battery_stored",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
SensorEntityDescription(
key="energy_battery_consumed",
translation_key="energy_battery_consumed",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=2,
),
)

View File

@@ -35,9 +35,20 @@
"battery_soc": {
"name": "Battery state of charge"
},
"battery_status": {
"name": "Battery status",
"state": {
"charged": "Charged",
"charging": "[%key:common::state::charging%]",
"discharging": "[%key:common::state::discharging%]"
}
},
"battery_stored": {
"name": "Battery stored"
},
"battery_consumed": {
"name": "Battery consumed"
},
"grid_current_l1": {
"name": "Grid current L1"
},
@@ -77,6 +88,15 @@
"inverter_injection_power_limit": {
"name": "Injection power limit"
},
"manager_inverter_state": {
"name": "Inverter state",
"state": {
"unsynchronized": "Unsynchronized",
"grid_consumption": "Grid consumption",
"grid_injection": "Grid injection",
"grid_synchronised_but_not_used": "Grid unsynchronized but used"
}
},
"meter_power": {
"name": "Meter power"
},
@@ -135,25 +155,63 @@
"name": "Component temperature"
},
"monitoring_self_consumption": {
"name": "Monitoring self-consumption"
"name": "Self-consumption"
},
"monitoring_self_sufficiency": {
"name": "Monitoring self-sufficiency"
"name": "Self-sufficiency"
},
"monitoring_minute_building_consumption": {
"name": "Monitoring building consumption (minute)"
"name": "Building consumption"
},
"monitoring_minute_grid_consumption": {
"name": "Monitoring grid consumption (minute)"
"name": "Grid consumption"
},
"monitoring_minute_grid_injection": {
"name": "Monitoring grid injection (minute)"
"name": "Grid injection"
},
"monitoring_minute_grid_power_flow": {
"name": "Monitoring grid power flow (minute)"
"name": "Grid power flow"
},
"monitoring_minute_solar_production": {
"name": "Monitoring solar production (minute)"
"name": "Solar production"
},
"timeline_type_msg": {
"name": "Timeline status",
"state": {
"com_lost": "Communication lost.",
"warning_grid": "Power grid warning detected.",
"warning_pv": "PV system warning detected.",
"warning_bat": "Battery warning detected.",
"error_ond": "Inverter error detected.",
"error_soft": "Software error detected.",
"error_pv": "PV system error detected.",
"error_grid": "Power grid error detected.",
"error_bat": "Battery error detected.",
"good_1": "System operating normally.",
"web_account": "Web account notification.",
"info_soft": "Software information available.",
"info_ond": "Inverter information available.",
"info_bat": "Battery information available.",
"info_smartlo": "Smart load information available."
}
},
"energy_pv": {
"name": "Today PV energy"
},
"energy_grid_injected": {
"name": "Today grid-injected energy"
},
"energy_grid_consumed": {
"name": "Today grid-consumed energy"
},
"energy_building_consumption": {
"name": "Today building consumption"
},
"energy_battery_stored": {
"name": "Today battery-stored energy"
},
"energy_battery_consumed": {
"name": "Today battery-consumed energy"
}
}
}

View File

@@ -66,7 +66,7 @@ def mock_imeon_inverter() -> Generator[MagicMock]:
"serial": TEST_SERIAL,
"url": f"http://{TEST_USER_INPUT[CONF_HOST]}",
}
inverter.storage = load_json_object_fixture("sensor_data.json", DOMAIN)
inverter.storage = load_json_object_fixture("entity_data.json", DOMAIN)
yield inverter

View File

@@ -0,0 +1,79 @@
{
"battery": {
"battery_power": 2500.0,
"battery_soc": 78.0,
"battery_status": "charging",
"battery_stored": 10200.0,
"battery_consumed": 500.0
},
"grid": {
"grid_current_l1": 12.5,
"grid_current_l2": 10.8,
"grid_current_l3": 11.2,
"grid_frequency": 50.0,
"grid_voltage_l1": 230.0,
"grid_voltage_l2": 229.5,
"grid_voltage_l3": 230.1
},
"input": {
"input_power_l1": 1000.0,
"input_power_l2": 950.0,
"input_power_l3": 980.0,
"input_power_total": 2930.0
},
"inverter": {
"inverter_charging_current_limit": 50,
"inverter_injection_power_limit": 5000.0,
"manager_inverter_state": "grid_consumption"
},
"meter": {
"meter_power": 2000.0
},
"output": {
"output_current_l1": 15.0,
"output_current_l2": 14.5,
"output_current_l3": 15.2,
"output_frequency": 49.9,
"output_power_l1": 1100.0,
"output_power_l2": 1080.0,
"output_power_l3": 1120.0,
"output_power_total": 3300.0,
"output_voltage_l1": 231.0,
"output_voltage_l2": 229.8,
"output_voltage_l3": 230.2
},
"pv": {
"pv_consumed": 1500.0,
"pv_injected": 800.0,
"pv_power_1": 1200.0,
"pv_power_2": 1300.0,
"pv_power_total": 2500.0
},
"temp": {
"temp_air_temperature": 25.0,
"temp_component_temperature": 45.5
},
"monitoring": {
"monitoring_self_produced": 2600.0,
"monitoring_self_consumption": 85.0,
"monitoring_self_sufficiency": 90.0
},
"monitoring_minute": {
"monitoring_minute_building_consumption": 50.0,
"monitoring_minute_grid_consumption": 8.3,
"monitoring_minute_grid_injection": 11.7,
"monitoring_minute_grid_power_flow": -3.4,
"monitoring_minute_solar_production": 43.3
},
"timeline": {
"timeline_type_msg": "info_bat"
},
"energy": {
"energy_pv": 12000.0,
"energy_grid_injected": 5000.0,
"energy_grid_consumed": 6000.0,
"energy_building_consumption": 15000.0,
"energy_battery_stored": 8000.0,
"energy_battery_consumed": 2000.0
}
}

View File

@@ -1,73 +0,0 @@
{
"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

@@ -1,16 +1,20 @@
"""Test the Imeon Inverter sensors."""
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from aiohttp import ClientError
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.components.imeon_inverter.coordinator import INTERVAL
from homeassistant.const import STATE_UNAVAILABLE, 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
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
async def test_sensors(
@@ -24,3 +28,51 @@ async def test_sensors(
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
"exception",
[
TimeoutError,
ClientError,
ValueError,
],
)
@pytest.mark.asyncio
async def test_sensor_unavailable_on_update_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_imeon_inverter: MagicMock,
freezer: FrozenDateTimeFactory,
exception: Exception,
) -> None:
"""Test that sensor becomes unavailable when update raises an error."""
entity_id = "sensor.imeon_inverter_battery_power"
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
mock_imeon_inverter.update.side_effect = exception
freezer.tick(INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNAVAILABLE
mock_imeon_inverter.update.side_effect = None
freezer.tick(INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE