mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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:
parent
ba8e2ed7d6
commit
5550dcbec8
@ -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]
|
||||
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
@ -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"],
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user