From f5809f5b873ba02e53d0ff549226e31585e566c5 Mon Sep 17 00:00:00 2001 From: darkrain-nl <24763370+darkrain-nl@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:16:43 +0200 Subject: [PATCH] Add Sofar settings sensors for the paired-register controls (#181197) --- homeassistant/components/sofar/sensor.py | 72 +++ homeassistant/components/sofar/strings.json | 33 ++ .../sofar/snapshots/test_sensor.ambr | 448 ++++++++++++++++++ tests/components/sofar/test_sensor.py | 4 +- 4 files changed, 555 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sofar/sensor.py b/homeassistant/components/sofar/sensor.py index 85912f8b55d8..9ba1c5338db2 100644 --- a/homeassistant/components/sofar/sensor.py +++ b/homeassistant/components/sofar/sensor.py @@ -7,6 +7,7 @@ from enum import IntEnum from typing import cast, override from sofar_modbus.modern.device import SofarInverter +from sofar_modbus.modern.enums import FeedinLimitationMode, PassiveModeTimeoutAction from homeassistant.components.sensor import ( RestoreSensor, @@ -1440,6 +1441,77 @@ SENSOR_DESCRIPTIONS: tuple[SofarSensorDescription, ...] = ( ], entity_category=EntityCategory.DIAGNOSTIC, ), + SofarSensorDescription( + key="feedin_limitation_mode", + component="feed_in", + translation_key="feedin_limitation_mode", + device_class=SensorDeviceClass.ENUM, + options=[mode.name.lower() for mode in FeedinLimitationMode], + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SofarSensorDescription( + key="feedin_max_power", + component="feed_in", + translation_key="feedin_max_power", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SofarSensorDescription( + key="active_power_export_limit", + component="active_power_control", + translation_key="active_power_export_limit", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SofarSensorDescription( + key="passive_mode_timeout", + component="passive", + translation_key="passive_mode_timeout", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SofarSensorDescription( + key="passive_mode_timeout_action", + component="passive", + translation_key="passive_mode_timeout_action", + device_class=SensorDeviceClass.ENUM, + options=[action.name.lower() for action in PassiveModeTimeoutAction], + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SofarSensorDescription( + key="passive_mode_grid_power", + component="passive", + translation_key="passive_mode_grid_power", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SofarSensorDescription( + key="passive_mode_battery_power_min", + component="passive", + translation_key="passive_mode_battery_power_min", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SofarSensorDescription( + key="passive_mode_battery_power_max", + component="passive", + translation_key="passive_mode_battery_power_max", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), ) SENSOR_DESCRIPTIONS += _part_sensors( diff --git a/homeassistant/components/sofar/strings.json b/homeassistant/components/sofar/strings.json index 632fca515627..027f6413ec91 100644 --- a/homeassistant/components/sofar/strings.json +++ b/homeassistant/components/sofar/strings.json @@ -131,6 +131,9 @@ } }, "sensor": { + "active_power_export_limit": { + "name": "Active power limit" + }, "active_power_load_sys": { "name": "Active power load system" }, @@ -317,6 +320,17 @@ "export_energy_total": { "name": "Export energy total" }, + "feedin_limitation_mode": { + "name": "Feed-in limitation mode", + "state": { + "disabled": "[%key:component::sofar::selector::feedin_limitation_mode::options::disabled%]", + "enabled_3_phase_limit": "[%key:component::sofar::selector::feedin_limitation_mode::options::enabled_3_phase_limit%]", + "enabled_feed_in_limitation": "[%key:component::sofar::selector::feedin_limitation_mode::options::enabled_feed_in_limitation%]" + } + }, + "feedin_max_power": { + "name": "Feed-in maximum power" + }, "grid_frequency": { "name": "Grid frequency" }, @@ -446,6 +460,25 @@ "passive_eps_wait_time": { "name": "EPS wait time" }, + "passive_mode_battery_power_max": { + "name": "Passive mode maximum battery power" + }, + "passive_mode_battery_power_min": { + "name": "Passive mode minimum battery power" + }, + "passive_mode_grid_power": { + "name": "Passive mode grid power" + }, + "passive_mode_timeout": { + "name": "Passive mode timeout" + }, + "passive_mode_timeout_action": { + "name": "Passive mode timeout action", + "state": { + "force_standby": "[%key:component::sofar::selector::passive_mode_timeout_action::options::force_standby%]", + "return_to_previous_mode": "[%key:component::sofar::selector::passive_mode_timeout_action::options::return_to_previous_mode%]" + } + }, "power": { "name": "Power" }, diff --git a/tests/components/sofar/snapshots/test_sensor.ambr b/tests/components/sofar/snapshots/test_sensor.ambr index 3cbc58f26106..110591fe22d9 100644 --- a/tests/components/sofar/snapshots/test_sensor.ambr +++ b/tests/components/sofar/snapshots/test_sensor.ambr @@ -769,6 +769,57 @@ 'state': '51.5', }) # --- +# name: test_all_entities[sensor.hydxxktl_3p_active_power_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_active_power_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active power limit', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active power limit', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'active_power_export_limit', + 'unique_id': 'SP1XXES100XX_active_power_export_limit', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_active_power_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'HYDxxKTL-3P Active power limit', + : '%', + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_active_power_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_entities[sensor.hydxxktl_3p_active_power_load_system-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -3747,6 +3798,123 @@ 'state': '0.0', }) # --- +# name: test_all_entities[sensor.hydxxktl_3p_feed_in_limitation_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'disabled', + 'enabled_feed_in_limitation', + 'enabled_3_phase_limit', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_feed_in_limitation_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Feed-in limitation mode', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Feed-in limitation mode', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'feedin_limitation_mode', + 'unique_id': 'SP1XXES100XX_feedin_limitation_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_feed_in_limitation_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'HYDxxKTL-3P Feed-in limitation mode', + : list([ + 'disabled', + 'enabled_feed_in_limitation', + 'enabled_3_phase_limit', + ]), + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_feed_in_limitation_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disabled', + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_feed_in_maximum_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_feed_in_maximum_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Feed-in maximum power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Feed-in maximum power', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'feedin_max_power', + 'unique_id': 'SP1XXES100XX_feedin_max_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_feed_in_maximum_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power', + : 'HYDxxKTL-3P Feed-in maximum power', + : , + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_feed_in_maximum_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_all_entities[sensor.hydxxktl_3p_grid_frequency-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -5823,6 +5991,286 @@ 'state': '0.0', }) # --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_grid_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_grid_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Passive mode grid power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Passive mode grid power', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'passive_mode_grid_power', + 'unique_id': 'SP1XXES100XX_passive_mode_grid_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_grid_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power', + : 'HYDxxKTL-3P Passive mode grid power', + : , + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_maximum_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_maximum_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Passive mode maximum battery power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Passive mode maximum battery power', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'passive_mode_battery_power_max', + 'unique_id': 'SP1XXES100XX_passive_mode_battery_power_max', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_maximum_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power', + : 'HYDxxKTL-3P Passive mode maximum battery power', + : , + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_maximum_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_minimum_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_minimum_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Passive mode minimum battery power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Passive mode minimum battery power', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'passive_mode_battery_power_min', + 'unique_id': 'SP1XXES100XX_passive_mode_battery_power_min', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_minimum_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'power', + : 'HYDxxKTL-3P Passive mode minimum battery power', + : , + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_minimum_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_timeout-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_timeout', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Passive mode timeout', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Passive mode timeout', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'passive_mode_timeout', + 'unique_id': 'SP1XXES100XX_passive_mode_timeout', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_timeout-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'duration', + : 'HYDxxKTL-3P Passive mode timeout', + : , + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_timeout', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_timeout_action-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : list([ + 'force_standby', + 'return_to_previous_mode', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_timeout_action', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Passive mode timeout action', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Passive mode timeout action', + 'platform': 'sofar', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'passive_mode_timeout_action', + 'unique_id': 'SP1XXES100XX_passive_mode_timeout_action', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.hydxxktl_3p_passive_mode_timeout_action-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'enum', + : 'HYDxxKTL-3P Passive mode timeout action', + : list([ + 'force_standby', + 'return_to_previous_mode', + ]), + }), + 'context': , + 'entity_id': 'sensor.hydxxktl_3p_passive_mode_timeout_action', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'force_standby', + }) +# --- # name: test_all_entities[sensor.hydxxktl_3p_power_factor_output_l1-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/sofar/test_sensor.py b/tests/components/sofar/test_sensor.py index 5e37e43f0c1d..413421cfabce 100644 --- a/tests/components/sofar/test_sensor.py +++ b/tests/components/sofar/test_sensor.py @@ -102,12 +102,12 @@ async def test_sensor_entities_created_and_state( @pytest.mark.parametrize( ("serial", "model", "seed", "created", "enabled"), [ - pytest.param(MOCK_SERIAL, MOCK_MODEL, seed_pv_inverter, 71, 21, id="pv"), + pytest.param(MOCK_SERIAL, MOCK_MODEL, seed_pv_inverter, 74, 21, id="pv"), pytest.param( MOCK_HYBRID_SERIAL, MOCK_HYBRID_MODEL, seed_hybrid_inverter, - 137, + 145, 44, id="hybrid", ),