diff --git a/homeassistant/components/fronius/const.py b/homeassistant/components/fronius/const.py index 4060731b21cf66..18f35de8336c4c 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -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] diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index 94fd5f256aad4d..fcf9ce0a389b54 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -49,8 +49,10 @@ def __init__(self, *args: Any, solar_net: FroniusSolarNet, **kwargs: Any) -> Non """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 @@ async def _async_update_data(self) -> dict[SolarNetId, Any]: 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 @@ def add_entities_for_seen_keys( """ @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 - ): + 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 if device_data[key]["value"] is None: + remaining_unregistered_descriptors.append(description) continue - new_entities.append(entity_constructor(self, key, solar_net_id)) - self.unregistered_keys[solar_net_id].remove(key) + 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) ) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index f11855ce7e2277..f058a25a0443ac 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -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.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 @@ class FroniusSensorEntityDescription(SensorEntityDescription): 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 @@ class FroniusSensorEntityDescription(SensorEntityDescription): 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 @@ class FroniusSensorEntityDescription(SensorEntityDescription): ), 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 @@ def _device_data(self) -> dict[str, Any]: 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 @@ def _handle_coordinator_update(self) -> None: 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 @@ def __init__( 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 @@ def __init__( 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"], diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index 4a0f96ed8e61c6..de06670464424e 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -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" diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index f94b0f3a55c1bd..684e9a3ae5f4ce 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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 @@ def assert_state(entity_id, expected_state): 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)