Add textual representation entities for Fronius status codes (#94155)

* optionally decouple `EntityDescription.key` from API response key

this makes it possible to have multiple entities for a single API response field

* Add optional `value_fn` to EntityDescriptions

eg. to be able to map a API response value to a different value (status_code -> message)

* Add inverter `status_message` entity

* Add meter `meter_location_description` entity

* add external battery state

* Make Ohmpilot entity state translateable

* use built-in StrEnum

* test coverage

* remove unnecessary checks

None is handled before
This commit is contained in:
Matthias Alphart 2023-11-27 13:59:25 +01:00 committed by GitHub
parent ba8e2ed7d6
commit 5550dcbec8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 303 additions and 79 deletions

View File

@ -1,7 +1,9 @@
"""Constants for the Fronius integration."""
from enum import StrEnum
from typing import Final, NamedTuple, TypedDict
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.typing import StateType
DOMAIN: Final = "fronius"
@ -25,3 +27,97 @@ class FroniusDeviceInfo(NamedTuple):
device_info: DeviceInfo
solar_net_id: SolarNetId
unique_id: str
class InverterStatusCodeOption(StrEnum):
"""Status codes for Fronius inverters."""
# these are keys for state translations - so snake_case is used
STARTUP = "startup"
RUNNING = "running"
STANDBY = "standby"
BOOTLOADING = "bootloading"
ERROR = "error"
IDLE = "idle"
READY = "ready"
SLEEPING = "sleeping"
UNKNOWN = "unknown"
INVALID = "invalid"
_INVERTER_STATUS_CODES: Final[dict[int, InverterStatusCodeOption]] = {
0: InverterStatusCodeOption.STARTUP,
1: InverterStatusCodeOption.STARTUP,
2: InverterStatusCodeOption.STARTUP,
3: InverterStatusCodeOption.STARTUP,
4: InverterStatusCodeOption.STARTUP,
5: InverterStatusCodeOption.STARTUP,
6: InverterStatusCodeOption.STARTUP,
7: InverterStatusCodeOption.RUNNING,
8: InverterStatusCodeOption.STANDBY,
9: InverterStatusCodeOption.BOOTLOADING,
10: InverterStatusCodeOption.ERROR,
11: InverterStatusCodeOption.IDLE,
12: InverterStatusCodeOption.READY,
13: InverterStatusCodeOption.SLEEPING,
255: InverterStatusCodeOption.UNKNOWN,
}
def get_inverter_status_message(code: StateType) -> InverterStatusCodeOption:
"""Return a status message for a given status code."""
return _INVERTER_STATUS_CODES.get(code, InverterStatusCodeOption.INVALID) # type: ignore[arg-type]
class MeterLocationCodeOption(StrEnum):
"""Meter location codes for Fronius meters."""
# these are keys for state translations - so snake_case is used
FEED_IN = "feed_in"
CONSUMPTION_PATH = "consumption_path"
GENERATOR = "external_generator"
EXT_BATTERY = "external_battery"
SUBLOAD = "subload"
def get_meter_location_description(code: StateType) -> MeterLocationCodeOption | None:
"""Return a location_description for a given location code."""
match int(code): # type: ignore[arg-type]
case 0:
return MeterLocationCodeOption.FEED_IN
case 1:
return MeterLocationCodeOption.CONSUMPTION_PATH
case 3:
return MeterLocationCodeOption.GENERATOR
case 4:
return MeterLocationCodeOption.EXT_BATTERY
case _ as _code if 256 <= _code <= 511:
return MeterLocationCodeOption.SUBLOAD
return None
class OhmPilotStateCodeOption(StrEnum):
"""OhmPilot state codes for Fronius inverters."""
# these are keys for state translations - so snake_case is used
UP_AND_RUNNING = "up_and_running"
KEEP_MINIMUM_TEMPERATURE = "keep_minimum_temperature"
LEGIONELLA_PROTECTION = "legionella_protection"
CRITICAL_FAULT = "critical_fault"
FAULT = "fault"
BOOST_MODE = "boost_mode"
_OHMPILOT_STATE_CODES: Final[dict[int, OhmPilotStateCodeOption]] = {
0: OhmPilotStateCodeOption.UP_AND_RUNNING,
1: OhmPilotStateCodeOption.KEEP_MINIMUM_TEMPERATURE,
2: OhmPilotStateCodeOption.LEGIONELLA_PROTECTION,
3: OhmPilotStateCodeOption.CRITICAL_FAULT,
4: OhmPilotStateCodeOption.FAULT,
5: OhmPilotStateCodeOption.BOOST_MODE,
}
def get_ohmpilot_state_message(code: StateType) -> OhmPilotStateCodeOption | None:
"""Return a status message for a given status code."""
return _OHMPILOT_STATE_CODES.get(code) # type: ignore[arg-type]

View File

@ -49,8 +49,10 @@ class FroniusCoordinatorBase(
"""Set up the FroniusCoordinatorBase class."""
self._failed_update_count = 0
self.solar_net = solar_net
# unregistered_keys are used to create entities in platform module
self.unregistered_keys: dict[SolarNetId, set[str]] = {}
# unregistered_descriptors are used to create entities in platform module
self.unregistered_descriptors: dict[
SolarNetId, list[FroniusSensorEntityDescription]
] = {}
super().__init__(*args, update_interval=self.default_interval, **kwargs)
@abstractmethod
@ -73,11 +75,11 @@ class FroniusCoordinatorBase(
self.update_interval = self.default_interval
for solar_net_id in data:
if solar_net_id not in self.unregistered_keys:
if solar_net_id not in self.unregistered_descriptors:
# id seen for the first time
self.unregistered_keys[solar_net_id] = {
desc.key for desc in self.valid_descriptions
}
self.unregistered_descriptors[
solar_net_id
] = self.valid_descriptions.copy()
return data
@callback
@ -92,22 +94,34 @@ class FroniusCoordinatorBase(
"""
@callback
def _add_entities_for_unregistered_keys() -> None:
def _add_entities_for_unregistered_descriptors() -> None:
"""Add entities for keys seen for the first time."""
new_entities: list = []
new_entities: list[_FroniusEntityT] = []
for solar_net_id, device_data in self.data.items():
for key in self.unregistered_keys[solar_net_id].intersection(
device_data
):
if device_data[key]["value"] is None:
remaining_unregistered_descriptors = []
for description in self.unregistered_descriptors[solar_net_id]:
key = description.response_key or description.key
if key not in device_data:
remaining_unregistered_descriptors.append(description)
continue
new_entities.append(entity_constructor(self, key, solar_net_id))
self.unregistered_keys[solar_net_id].remove(key)
if device_data[key]["value"] is None:
remaining_unregistered_descriptors.append(description)
continue
new_entities.append(
entity_constructor(
coordinator=self,
description=description,
solar_net_id=solar_net_id,
)
)
self.unregistered_descriptors[
solar_net_id
] = remaining_unregistered_descriptors
async_add_entities(new_entities)
_add_entities_for_unregistered_keys()
_add_entities_for_unregistered_descriptors()
self.solar_net.cleanup_callbacks.append(
self.async_add_listener(_add_entities_for_unregistered_keys)
self.async_add_listener(_add_entities_for_unregistered_descriptors)
)

View File

@ -1,6 +1,7 @@
"""Support for Fronius devices."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Final
@ -30,7 +31,16 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, SOLAR_NET_DISCOVERY_NEW
from .const import (
DOMAIN,
SOLAR_NET_DISCOVERY_NEW,
InverterStatusCodeOption,
MeterLocationCodeOption,
OhmPilotStateCodeOption,
get_inverter_status_message,
get_meter_location_description,
get_ohmpilot_state_message,
)
if TYPE_CHECKING:
from . import FroniusSolarNet
@ -102,6 +112,8 @@ class FroniusSensorEntityDescription(SensorEntityDescription):
# Gen24 devices may report 0 for total energy while doing firmware updates.
# Handling such values shall mitigate spikes in delta calculations.
invalid_when_falsy: bool = False
response_key: str | None = None
value_fn: Callable[[StateType], StateType] | None = None
INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
@ -198,6 +210,15 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
FroniusSensorEntityDescription(
key="status_code",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
FroniusSensorEntityDescription(
key="status_message",
response_key="status_code",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[opt.value for opt in InverterStatusCodeOption],
value_fn=get_inverter_status_message,
),
FroniusSensorEntityDescription(
key="led_state",
@ -306,6 +327,15 @@ METER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
FroniusSensorEntityDescription(
key="meter_location",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=int, # type: ignore[arg-type]
),
FroniusSensorEntityDescription(
key="meter_location_description",
response_key="meter_location",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[opt.value for opt in MeterLocationCodeOption],
value_fn=get_meter_location_description,
),
FroniusSensorEntityDescription(
key="power_apparent_phase_1",
@ -495,7 +525,11 @@ OHMPILOT_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
),
FroniusSensorEntityDescription(
key="state_message",
response_key="state_code",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=[opt.value for opt in OhmPilotStateCodeOption],
value_fn=get_ohmpilot_state_message,
),
]
@ -630,24 +664,22 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
"""Defines a Fronius coordinator entity."""
entity_description: FroniusSensorEntityDescription
entity_descriptions: list[FroniusSensorEntityDescription]
_attr_has_entity_name = True
def __init__(
self,
coordinator: FroniusCoordinatorBase,
key: str,
description: FroniusSensorEntityDescription,
solar_net_id: str,
) -> None:
"""Set up an individual Fronius meter sensor."""
super().__init__(coordinator)
self.entity_description = next(
desc for desc in self.entity_descriptions if desc.key == key
)
self.entity_description = description
self.response_key = description.response_key or description.key
self.solar_net_id = solar_net_id
self._attr_native_value = self._get_entity_value()
self._attr_translation_key = self.entity_description.key
self._attr_translation_key = description.key
def _device_data(self) -> dict[str, Any]:
"""Extract information for SolarNet device from coordinator data."""
@ -655,13 +687,13 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
def _get_entity_value(self) -> Any:
"""Extract entity value from coordinator. Raises KeyError if not included in latest update."""
new_value = self.coordinator.data[self.solar_net_id][
self.entity_description.key
]["value"]
new_value = self.coordinator.data[self.solar_net_id][self.response_key]["value"]
if new_value is None:
return self.entity_description.default_value
if self.entity_description.invalid_when_falsy and not new_value:
return None
if self.entity_description.value_fn is not None:
return self.entity_description.value_fn(new_value)
if isinstance(new_value, float):
return round(new_value, 4)
return new_value
@ -681,54 +713,54 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
class InverterSensor(_FroniusSensorEntity):
"""Defines a Fronius inverter device sensor entity."""
entity_descriptions = INVERTER_ENTITY_DESCRIPTIONS
def __init__(
self,
coordinator: FroniusInverterUpdateCoordinator,
key: str,
description: FroniusSensorEntityDescription,
solar_net_id: str,
) -> None:
"""Set up an individual Fronius inverter sensor."""
super().__init__(coordinator, key, solar_net_id)
super().__init__(coordinator, description, solar_net_id)
# device_info created in __init__ from a `GetInverterInfo` request
self._attr_device_info = coordinator.inverter_info.device_info
self._attr_unique_id = f"{coordinator.inverter_info.unique_id}-{key}"
self._attr_unique_id = (
f"{coordinator.inverter_info.unique_id}-{description.key}"
)
class LoggerSensor(_FroniusSensorEntity):
"""Defines a Fronius logger device sensor entity."""
entity_descriptions = LOGGER_ENTITY_DESCRIPTIONS
def __init__(
self,
coordinator: FroniusLoggerUpdateCoordinator,
key: str,
description: FroniusSensorEntityDescription,
solar_net_id: str,
) -> None:
"""Set up an individual Fronius meter sensor."""
super().__init__(coordinator, key, solar_net_id)
super().__init__(coordinator, description, solar_net_id)
logger_data = self._device_data()
# Logger device is already created in FroniusSolarNet._create_solar_net_device
self._attr_device_info = coordinator.solar_net.system_device_info
self._attr_native_unit_of_measurement = logger_data[key].get("unit")
self._attr_unique_id = f'{logger_data["unique_identifier"]["value"]}-{key}'
self._attr_native_unit_of_measurement = logger_data[self.response_key].get(
"unit"
)
self._attr_unique_id = (
f'{logger_data["unique_identifier"]["value"]}-{description.key}'
)
class MeterSensor(_FroniusSensorEntity):
"""Defines a Fronius meter device sensor entity."""
entity_descriptions = METER_ENTITY_DESCRIPTIONS
def __init__(
self,
coordinator: FroniusMeterUpdateCoordinator,
key: str,
description: FroniusSensorEntityDescription,
solar_net_id: str,
) -> None:
"""Set up an individual Fronius meter sensor."""
super().__init__(coordinator, key, solar_net_id)
super().__init__(coordinator, description, solar_net_id)
meter_data = self._device_data()
# S0 meters connected directly to inverters respond "n.a." as serial number
# `model` contains the inverter id: "S0 Meter at inverter 1"
@ -745,22 +777,20 @@ class MeterSensor(_FroniusSensorEntity):
name=meter_data["model"]["value"],
via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id),
)
self._attr_unique_id = f"{meter_uid}-{key}"
self._attr_unique_id = f"{meter_uid}-{description.key}"
class OhmpilotSensor(_FroniusSensorEntity):
"""Defines a Fronius Ohmpilot sensor entity."""
entity_descriptions = OHMPILOT_ENTITY_DESCRIPTIONS
def __init__(
self,
coordinator: FroniusOhmpilotUpdateCoordinator,
key: str,
description: FroniusSensorEntityDescription,
solar_net_id: str,
) -> None:
"""Set up an individual Fronius meter sensor."""
super().__init__(coordinator, key, solar_net_id)
super().__init__(coordinator, description, solar_net_id)
device_data = self._device_data()
self._attr_device_info = DeviceInfo(
@ -771,45 +801,41 @@ class OhmpilotSensor(_FroniusSensorEntity):
sw_version=device_data["software"]["value"],
via_device=(DOMAIN, coordinator.solar_net.solar_net_device_id),
)
self._attr_unique_id = f'{device_data["serial"]["value"]}-{key}'
self._attr_unique_id = f'{device_data["serial"]["value"]}-{description.key}'
class PowerFlowSensor(_FroniusSensorEntity):
"""Defines a Fronius power flow sensor entity."""
entity_descriptions = POWER_FLOW_ENTITY_DESCRIPTIONS
def __init__(
self,
coordinator: FroniusPowerFlowUpdateCoordinator,
key: str,
description: FroniusSensorEntityDescription,
solar_net_id: str,
) -> None:
"""Set up an individual Fronius power flow sensor."""
super().__init__(coordinator, key, solar_net_id)
super().__init__(coordinator, description, solar_net_id)
# SolarNet device is already created in FroniusSolarNet._create_solar_net_device
self._attr_device_info = coordinator.solar_net.system_device_info
self._attr_unique_id = (
f"{coordinator.solar_net.solar_net_device_id}-power_flow-{key}"
f"{coordinator.solar_net.solar_net_device_id}-power_flow-{description.key}"
)
class StorageSensor(_FroniusSensorEntity):
"""Defines a Fronius storage device sensor entity."""
entity_descriptions = STORAGE_ENTITY_DESCRIPTIONS
def __init__(
self,
coordinator: FroniusStorageUpdateCoordinator,
key: str,
description: FroniusSensorEntityDescription,
solar_net_id: str,
) -> None:
"""Set up an individual Fronius storage sensor."""
super().__init__(coordinator, key, solar_net_id)
super().__init__(coordinator, description, solar_net_id)
storage_data = self._device_data()
self._attr_unique_id = f'{storage_data["serial"]["value"]}-{key}'
self._attr_unique_id = f'{storage_data["serial"]["value"]}-{description.key}'
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, storage_data["serial"]["value"])},
manufacturer=storage_data["manufacturer"]["value"],

View File

@ -66,6 +66,21 @@
"status_code": {
"name": "Status code"
},
"status_message": {
"name": "Status message",
"state": {
"startup": "Startup",
"running": "Running",
"standby": "Standby",
"bootloading": "Bootloading",
"error": "Error",
"idle": "Idle",
"ready": "Ready",
"sleeping": "Sleeping",
"unknown": "Unknown",
"invalid": "Invalid"
}
},
"led_state": {
"name": "LED state"
},
@ -114,6 +129,16 @@
"meter_location": {
"name": "Meter location"
},
"meter_location_description": {
"name": "Meter location description",
"state": {
"feed_in": "Grid interconnection point",
"consumption_path": "Consumption path",
"external_generator": "External generator",
"external_battery": "External battery",
"subload": "Subload"
}
},
"power_apparent_phase_1": {
"name": "Apparent power phase 1"
},
@ -193,7 +218,15 @@
"name": "State code"
},
"state_message": {
"name": "State message"
"name": "State message",
"state": {
"up_and_running": "Up and running",
"keep_minimum_temperature": "Keep minimum temperature",
"legionella_protection": "Legionella protection",
"critical_fault": "Critical fault",
"fault": "Fault",
"boost_mode": "Boost mode"
}
},
"meter_mode": {
"name": "Meter mode"

View File

@ -1,6 +1,6 @@
"""Tests for the Fronius sensor platform."""
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.fronius.const import DOMAIN
from homeassistant.components.fronius.coordinator import (
@ -33,33 +33,34 @@ async def test_symo_inverter(
mock_responses(aioclient_mock, night=True)
config_entry = await setup_fronius_integration(hass)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 21
await enable_all_entities(
hass,
freezer,
config_entry.entry_id,
FroniusInverterUpdateCoordinator.default_interval,
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54
assert_state("sensor.symo_20_dc_current", 0)
assert_state("sensor.symo_20_energy_day", 10828)
assert_state("sensor.symo_20_total_energy", 44186900)
assert_state("sensor.symo_20_energy_year", 25507686)
assert_state("sensor.symo_20_dc_voltage", 16)
assert_state("sensor.symo_20_status_message", "startup")
# Second test at daytime when inverter is producing
mock_responses(aioclient_mock, night=False)
freezer.tick(FroniusInverterUpdateCoordinator.default_interval)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58
await enable_all_entities(
hass,
freezer,
config_entry.entry_id,
FroniusInverterUpdateCoordinator.default_interval,
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60
# 4 additional AC entities
assert_state("sensor.symo_20_dc_current", 2.19)
assert_state("sensor.symo_20_energy_day", 1113)
@ -70,6 +71,7 @@ async def test_symo_inverter(
assert_state("sensor.symo_20_frequency", 49.94)
assert_state("sensor.symo_20_ac_power", 1190)
assert_state("sensor.symo_20_ac_voltage", 227.90)
assert_state("sensor.symo_20_status_message", "running")
# Third test at nighttime - additional AC entities default to 0
mock_responses(aioclient_mock, night=True)
@ -94,7 +96,7 @@ async def test_symo_logger(
mock_responses(aioclient_mock)
await setup_fronius_integration(hass)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25
# states are rounded to 4 decimals
assert_state("sensor.solarnet_grid_export_tariff", 0.078)
assert_state("sensor.solarnet_co2_factor", 0.53)
@ -116,14 +118,14 @@ async def test_symo_meter(
mock_responses(aioclient_mock)
config_entry = await setup_fronius_integration(hass)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 25
await enable_all_entities(
hass,
freezer,
config_entry.entry_id,
FroniusMeterUpdateCoordinator.default_interval,
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 60
# states are rounded to 4 decimals
assert_state("sensor.smart_meter_63a_current_phase_1", 7.755)
assert_state("sensor.smart_meter_63a_current_phase_2", 6.68)
@ -157,6 +159,50 @@ async def test_symo_meter(
assert_state("sensor.smart_meter_63a_voltage_phase_1_2", 395.9)
assert_state("sensor.smart_meter_63a_voltage_phase_2_3", 398)
assert_state("sensor.smart_meter_63a_voltage_phase_3_1", 398)
assert_state("sensor.smart_meter_63a_meter_location", 0)
assert_state("sensor.smart_meter_63a_meter_location_description", "feed_in")
@pytest.mark.parametrize(
("location_code", "expected_code", "expected_description"),
[
(-1, -1, "unknown"),
(3, 3, "external_generator"),
(4, 4, "external_battery"),
(7, 7, "unknown"),
(256, 256, "subload"),
(511, 511, "subload"),
(512, 512, "unknown"),
],
)
async def test_symo_meter_forged(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
location_code: int | None,
expected_code: int | str,
expected_description: str,
) -> None:
"""Tests for meter location codes we have no fixture for."""
def assert_state(entity_id, expected_state):
state = hass.states.get(entity_id)
assert state
assert state.state == str(expected_state)
mock_responses(
aioclient_mock,
fixture_set="symo",
override_data={
"symo/GetMeterRealtimeData.json": [
(["Body", "Data", "0", "Meter_Location_Current"], location_code),
],
},
)
await setup_fronius_integration(hass)
assert_state("sensor.smart_meter_63a_meter_location", expected_code)
assert_state(
"sensor.smart_meter_63a_meter_location_description", expected_description
)
async def test_symo_power_flow(
@ -175,14 +221,14 @@ async def test_symo_power_flow(
mock_responses(aioclient_mock, night=True)
config_entry = await setup_fronius_integration(hass)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 21
await enable_all_entities(
hass,
freezer,
config_entry.entry_id,
FroniusInverterUpdateCoordinator.default_interval,
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54
# states are rounded to 4 decimals
assert_state("sensor.solarnet_energy_day", 10828)
assert_state("sensor.solarnet_total_energy", 44186900)
@ -197,7 +243,7 @@ async def test_symo_power_flow(
async_fire_time_changed(hass)
await hass.async_block_till_done()
# 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56
assert_state("sensor.solarnet_energy_day", 1101.7001)
assert_state("sensor.solarnet_total_energy", 44188000)
assert_state("sensor.solarnet_energy_year", 25508788)
@ -212,7 +258,7 @@ async def test_symo_power_flow(
freezer.tick(FroniusPowerFlowUpdateCoordinator.default_interval)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56
assert_state("sensor.solarnet_energy_day", 10828)
assert_state("sensor.solarnet_total_energy", 44186900)
assert_state("sensor.solarnet_energy_year", 25507686)
@ -238,18 +284,19 @@ async def test_gen24(
mock_responses(aioclient_mock, fixture_set="gen24")
config_entry = await setup_fronius_integration(hass, is_logger=False)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 22
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 23
await enable_all_entities(
hass,
freezer,
config_entry.entry_id,
FroniusMeterUpdateCoordinator.default_interval,
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54
# inverter 1
assert_state("sensor.inverter_name_ac_current", 0.1589)
assert_state("sensor.inverter_name_dc_current_2", 0.0754)
assert_state("sensor.inverter_name_status_code", 7)
assert_state("sensor.inverter_name_status_message", "running")
assert_state("sensor.inverter_name_dc_current", 0.0783)
assert_state("sensor.inverter_name_dc_voltage_2", 403.4312)
assert_state("sensor.inverter_name_ac_power", 37.3204)
@ -264,7 +311,8 @@ async def test_gen24(
assert_state("sensor.smart_meter_ts_65a_3_real_energy_consumed", 2013105.0)
assert_state("sensor.smart_meter_ts_65a_3_real_power", 653.1)
assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9)
assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0)
assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0)
assert_state("sensor.smart_meter_ts_65a_3_meter_location_description", "feed_in")
assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.828)
assert_state("sensor.smart_meter_ts_65a_3_reactive_energy_consumed", 88221.0)
assert_state("sensor.smart_meter_ts_65a_3_real_energy_minus", 3863340.0)
@ -336,14 +384,14 @@ async def test_gen24_storage(
hass, is_logger=False, unique_id="12345678"
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 34
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 35
await enable_all_entities(
hass,
freezer,
config_entry.entry_id,
FroniusMeterUpdateCoordinator.default_interval,
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 66
# inverter 1
assert_state("sensor.gen24_storage_dc_current", 0.3952)
assert_state("sensor.gen24_storage_dc_voltage_2", 318.8103)
@ -352,6 +400,7 @@ async def test_gen24_storage(
assert_state("sensor.gen24_storage_ac_power", 250.9093)
assert_state("sensor.gen24_storage_error_code", 0)
assert_state("sensor.gen24_storage_status_code", 7)
assert_state("sensor.gen24_storage_status_message", "running")
assert_state("sensor.gen24_storage_total_energy", 7512794.0117)
assert_state("sensor.gen24_storage_inverter_state", "Running")
assert_state("sensor.gen24_storage_dc_voltage", 419.1009)
@ -363,7 +412,8 @@ async def test_gen24_storage(
assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.698)
assert_state("sensor.smart_meter_ts_65a_3_real_energy_consumed", 1247204.0)
assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9)
assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0)
assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0)
assert_state("sensor.smart_meter_ts_65a_3_meter_location_description", "feed_in")
assert_state("sensor.smart_meter_ts_65a_3_reactive_power", -501.5)
assert_state("sensor.smart_meter_ts_65a_3_reactive_energy_produced", 3266105.0)
assert_state("sensor.smart_meter_ts_65a_3_real_power_phase_3", 19.6)
@ -396,7 +446,7 @@ async def test_gen24_storage(
assert_state("sensor.ohmpilot_power", 0.0)
assert_state("sensor.ohmpilot_temperature", 38.9)
assert_state("sensor.ohmpilot_state_code", 0.0)
assert_state("sensor.ohmpilot_state_message", "Up and running")
assert_state("sensor.ohmpilot_state_message", "up_and_running")
# power_flow
assert_state("sensor.solarnet_power_grid", 2274.9)
assert_state("sensor.solarnet_power_battery", 0.1591)
@ -463,14 +513,14 @@ async def test_primo_s0(
mock_responses(aioclient_mock, fixture_set="primo_s0", inverter_ids=[1, 2])
config_entry = await setup_fronius_integration(hass, is_logger=True)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 29
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 30
await enable_all_entities(
hass,
freezer,
config_entry.entry_id,
FroniusMeterUpdateCoordinator.default_interval,
)
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 40
assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 43
# logger
assert_state("sensor.solarnet_grid_export_tariff", 1)
assert_state("sensor.solarnet_co2_factor", 0.53)
@ -483,6 +533,7 @@ async def test_primo_s0(
assert_state("sensor.primo_5_0_1_error_code", 0)
assert_state("sensor.primo_5_0_1_dc_current", 4.23)
assert_state("sensor.primo_5_0_1_status_code", 7)
assert_state("sensor.primo_5_0_1_status_message", "running")
assert_state("sensor.primo_5_0_1_energy_year", 7532755.5)
assert_state("sensor.primo_5_0_1_ac_current", 3.85)
assert_state("sensor.primo_5_0_1_ac_voltage", 223.9)
@ -497,6 +548,7 @@ async def test_primo_s0(
assert_state("sensor.primo_3_0_1_error_code", 0)
assert_state("sensor.primo_3_0_1_dc_current", 0.97)
assert_state("sensor.primo_3_0_1_status_code", 7)
assert_state("sensor.primo_3_0_1_status_message", "running")
assert_state("sensor.primo_3_0_1_energy_year", 3596193.25)
assert_state("sensor.primo_3_0_1_ac_current", 1.32)
assert_state("sensor.primo_3_0_1_ac_voltage", 223.6)
@ -505,6 +557,9 @@ async def test_primo_s0(
assert_state("sensor.primo_3_0_1_led_state", 0)
# meter
assert_state("sensor.s0_meter_at_inverter_1_meter_location", 1)
assert_state(
"sensor.s0_meter_at_inverter_1_meter_location_description", "consumption_path"
)
assert_state("sensor.s0_meter_at_inverter_1_real_power", -2216.7487)
# power_flow
assert_state("sensor.solarnet_power_load", -2218.9349)