diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 2b863fbb505..cf648d3b613 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -22,6 +22,7 @@ from .coordinator import ( FroniusInverterUpdateCoordinator, FroniusLoggerUpdateCoordinator, FroniusMeterUpdateCoordinator, + FroniusOhmpilotUpdateCoordinator, FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) @@ -83,6 +84,7 @@ class FroniusSolarNet: self.inverter_coordinators: list[FroniusInverterUpdateCoordinator] = [] self.logger_coordinator: FroniusLoggerUpdateCoordinator | None = None self.meter_coordinator: FroniusMeterUpdateCoordinator | None = None + self.ohmpilot_coordinator: FroniusOhmpilotUpdateCoordinator | None = None self.power_flow_coordinator: FroniusPowerFlowUpdateCoordinator | None = None self.storage_coordinator: FroniusStorageUpdateCoordinator | None = None @@ -121,6 +123,15 @@ class FroniusSolarNet: ) ) + self.ohmpilot_coordinator = await self._init_optional_coordinator( + FroniusOhmpilotUpdateCoordinator( + hass=self.hass, + solar_net=self, + logger=_LOGGER, + name=f"{DOMAIN}_ohmpilot_{self.host}", + ) + ) + self.power_flow_coordinator = await self._init_optional_coordinator( FroniusPowerFlowUpdateCoordinator( hass=self.hass, diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index 7a8d156dd65..e89f828f47d 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -22,6 +22,7 @@ from .sensor import ( INVERTER_ENTITY_DESCRIPTIONS, LOGGER_ENTITY_DESCRIPTIONS, METER_ENTITY_DESCRIPTIONS, + OHMPILOT_ENTITY_DESCRIPTIONS, POWER_FLOW_ENTITY_DESCRIPTIONS, STORAGE_ENTITY_DESCRIPTIONS, ) @@ -158,6 +159,19 @@ class FroniusMeterUpdateCoordinator(FroniusCoordinatorBase): return data["meters"] # type: ignore[no-any-return] +class FroniusOhmpilotUpdateCoordinator(FroniusCoordinatorBase): + """Query Fronius Ohmpilots and keep track of seen conditions.""" + + default_interval = timedelta(minutes=1) + error_interval = timedelta(minutes=10) + valid_descriptions = OHMPILOT_ENTITY_DESCRIPTIONS + + async def _update_method(self) -> dict[SolarNetId, Any]: + """Return data per solar net id from pyfronius.""" + data = await self.solar_net.fronius.current_system_ohmpilot_data() + return data["ohmpilots"] # type: ignore[no-any-return] + + class FroniusPowerFlowUpdateCoordinator(FroniusCoordinatorBase): """Query Fronius power flow endpoint and keep track of seen conditions.""" diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index fa2a33ca7ea..a20f49962a5 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -52,6 +52,7 @@ if TYPE_CHECKING: FroniusInverterUpdateCoordinator, FroniusLoggerUpdateCoordinator, FroniusMeterUpdateCoordinator, + FroniusOhmpilotUpdateCoordinator, FroniusPowerFlowUpdateCoordinator, FroniusStorageUpdateCoordinator, ) @@ -110,6 +111,10 @@ async def async_setup_entry( solar_net.meter_coordinator.add_entities_for_seen_keys( async_add_entities, MeterSensor ) + if solar_net.ohmpilot_coordinator is not None: + solar_net.ohmpilot_coordinator.add_entities_for_seen_keys( + async_add_entities, OhmpilotSensor + ) if solar_net.power_flow_coordinator is not None: solar_net.power_flow_coordinator.add_entities_for_seen_keys( async_add_entities, PowerFlowSensor @@ -510,6 +515,45 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), ] +OHMPILOT_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="energy_real_ac_consumed", + name="Energy consumed", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="power_real_ac", + name="Power", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="temperature_channel_1", + name="Temperature Channel 1", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="error_code", + name="Error code", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="state_code", + name="State code", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key="state_message", + name="State message", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +] + POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ SensorEntityDescription( key="energy_day", @@ -760,6 +804,33 @@ class MeterSensor(_FroniusSensorEntity): self._attr_unique_id = f'{meter_data["serial"]["value"]}-{key}' +class OhmpilotSensor(_FroniusSensorEntity): + """Defines a Fronius Ohmpilot sensor entity.""" + + entity_descriptions = OHMPILOT_ENTITY_DESCRIPTIONS + + def __init__( + self, + coordinator: FroniusOhmpilotUpdateCoordinator, + key: str, + solar_net_id: str, + ) -> None: + """Set up an individual Fronius meter sensor.""" + self._entity_id_prefix = f"ohmpilot_{solar_net_id}" + super().__init__(coordinator, key, solar_net_id) + device_data = self._device_data() + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_data["serial"]["value"])}, + manufacturer=device_data["manufacturer"]["value"], + model=f"{device_data['model']['value']} {device_data['hardware']['value']}", + name=device_data["model"]["value"], + 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}' + + class PowerFlowSensor(_FroniusSensorEntity): """Defines a Fronius power flow sensor entity.""" diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 82ec7afd8ea..cf371a47471 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -373,11 +373,11 @@ async def test_gen24_storage(hass, aioclient_mock): mock_responses(aioclient_mock, fixture_set="gen24_storage") config_entry = await setup_fronius_integration(hass, is_logger=False) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 31 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 36 await enable_all_entities( hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval ) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 63 + assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 68 # inverter 1 assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.3952) assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 318.8103) @@ -437,6 +437,16 @@ async def test_gen24_storage(hass, aioclient_mock): assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 228.3) assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 821.9) assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 118.4) + # ohmpilot + assert_state( + "sensor.energy_real_ac_consumed_fronius_ohmpilot_0_http_fronius", 1233295.0 + ) + assert_state("sensor.power_real_ac_fronius_ohmpilot_0_http_fronius", 0.0) + assert_state("sensor.temperature_channel_1_fronius_ohmpilot_0_http_fronius", 38.9) + assert_state("sensor.state_code_fronius_ohmpilot_0_http_fronius", 0.0) + assert_state( + "sensor.state_message_fronius_ohmpilot_0_http_fronius", "Up and running" + ) # power_flow assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 2274.9) assert_state("sensor.power_battery_fronius_power_flow_0_http_fronius", 0.1591)