Add the Sofar fault sensors (#180905)

Co-authored-by: Erwin Douna <e.douna@gmail.com>
This commit is contained in:
darkrain-nl
2026-09-03 14:08:26 +02:00
committed by GitHub
co-authored by Erwin Douna
parent b3f43dea4a
commit 4a3c24e1d8
8 changed files with 1158 additions and 0 deletions
@@ -28,6 +28,7 @@ from .sensor import SENSOR_DESCRIPTIONS
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.SELECT,
Platform.SENSOR,
@@ -0,0 +1,82 @@
"""Support for Sofar binary sensors."""
from dataclasses import dataclass
from typing import override
from sofar_modbus.modern.faults import FaultCategory
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import SofarConfigEntry
from .entity import SofarEntity, SofarEntityDescription
PARALLEL_UPDATES = 0
_DISABLED_BY_DEFAULT = frozenset(
{
FaultCategory.ARC_FAULT,
FaultCategory.COMBINER_BOX,
FaultCategory.INPUT_FUSE,
FaultCategory.STRING_FUSE,
}
)
@dataclass(frozen=True, kw_only=True)
class SofarFaultBinarySensorDescription(
SofarEntityDescription, BinarySensorEntityDescription
):
"""Describe a Sofar fault-category binary sensor."""
category: FaultCategory
FAULT_SENSOR_DESCRIPTIONS: tuple[SofarFaultBinarySensorDescription, ...] = tuple(
SofarFaultBinarySensorDescription(
key=f"fault_{category.value}",
component="state",
translation_key=f"fault_{category.value}",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=category not in _DISABLED_BY_DEFAULT,
category=category,
)
for category in FaultCategory
)
async def async_setup_entry(
hass: HomeAssistant,
entry: SofarConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Sofar Inverter Modbus binary sensor platform."""
runtime_data = entry.runtime_data
served = runtime_data.served_components
async_add_entities(
SofarFaultBinarySensor(runtime_data, description)
for description in FAULT_SENSOR_DESCRIPTIONS
if description.component in served
)
class SofarFaultBinarySensor(SofarEntity, BinarySensorEntity):
"""Reports whether any fault in one subsystem is currently active."""
entity_description: SofarFaultBinarySensorDescription
@property
@override
def is_on(self) -> bool:
component = getattr(self.coordinator.device, self.entity_description.component)
return any(
fault.category is self.entity_description.category
for fault in component.active_faults
)
@@ -29,6 +29,7 @@ async def async_get_config_entry_diagnostics(
"serial_number": device.serial_number,
"readings_components": device.readings_components,
"settings_components": device.settings_components,
"active_faults": sorted(fault.key for fault in device.state.active_faults),
"raw": raw,
},
TO_REDACT,
@@ -46,6 +46,59 @@
}
},
"entity": {
"binary_sensor": {
"fault_ac_output": {
"name": "AC output fault"
},
"fault_arc_fault": {
"name": "Arc fault (AFCI)"
},
"fault_battery": {
"name": "Battery fault"
},
"fault_battery_pack": {
"name": "Battery pack communication fault"
},
"fault_combiner_box": {
"name": "Combiner box fault"
},
"fault_communication": {
"name": "Communication fault"
},
"fault_dc_bus": {
"name": "DC bus fault"
},
"fault_derating": {
"name": "Derating"
},
"fault_fan": {
"name": "Fan fault"
},
"fault_grid": {
"name": "Grid fault"
},
"fault_input_fuse": {
"name": "Input fuse open"
},
"fault_insulation": {
"name": "Insulation fault"
},
"fault_internal": {
"name": "Internal fault"
},
"fault_pv": {
"name": "PV fault"
},
"fault_shutdown": {
"name": "Shutdown"
},
"fault_string_fuse": {
"name": "String fuse open"
},
"fault_thermal": {
"name": "Thermal fault"
}
},
"button": {
"iv_curve_scan": {
"name": "IV curve scan"
@@ -0,0 +1,868 @@
# serializer version: 1
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_ac_output_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_ac_output_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'AC output fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'AC output fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_ac_output',
'unique_id': 'SS2ES104N5S445_fault_ac_output',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_ac_output_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 AC output fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_ac_output_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_arc_fault_afci-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_arc_fault_afci',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Arc fault (AFCI)',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Arc fault (AFCI)',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_arc_fault',
'unique_id': 'SS2ES104N5S445_fault_arc_fault',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_arc_fault_afci-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Arc fault (AFCI)',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_arc_fault_afci',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_battery_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Battery fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_battery',
'unique_id': 'SS2ES104N5S445_fault_battery',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Battery fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_battery_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_pack_communication_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_battery_pack_communication_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery pack communication fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Battery pack communication fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_battery_pack',
'unique_id': 'SS2ES104N5S445_fault_battery_pack',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_battery_pack_communication_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Battery pack communication fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_battery_pack_communication_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_combiner_box_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_combiner_box_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Combiner box fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Combiner box fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_combiner_box',
'unique_id': 'SS2ES104N5S445_fault_combiner_box',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_combiner_box_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Combiner box fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_combiner_box_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_communication_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_communication_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Communication fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Communication fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_communication',
'unique_id': 'SS2ES104N5S445_fault_communication',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_communication_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Communication fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_communication_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_dc_bus_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_dc_bus_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'DC bus fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'DC bus fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_dc_bus',
'unique_id': 'SS2ES104N5S445_fault_dc_bus',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_dc_bus_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 DC bus fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_dc_bus_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_derating-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_derating',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Derating',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Derating',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_derating',
'unique_id': 'SS2ES104N5S445_fault_derating',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_derating-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Derating',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_derating',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_fan_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_fan_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Fan fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Fan fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_fan',
'unique_id': 'SS2ES104N5S445_fault_fan',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_fan_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Fan fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_fan_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_grid_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_grid_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Grid fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Grid fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_grid',
'unique_id': 'SS2ES104N5S445_fault_grid',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_grid_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Grid fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_grid_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_input_fuse_open-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_input_fuse_open',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Input fuse open',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Input fuse open',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_input_fuse',
'unique_id': 'SS2ES104N5S445_fault_input_fuse',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_input_fuse_open-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Input fuse open',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_input_fuse_open',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_insulation_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_insulation_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Insulation fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Insulation fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_insulation',
'unique_id': 'SS2ES104N5S445_fault_insulation',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_insulation_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Insulation fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_insulation_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_internal_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_internal_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Internal fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Internal fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_internal',
'unique_id': 'SS2ES104N5S445_fault_internal',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_internal_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Internal fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_internal_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_pv_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_pv_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'PV fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'PV fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_pv',
'unique_id': 'SS2ES104N5S445_fault_pv',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_pv_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 PV fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_pv_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_shutdown-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_shutdown',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Shutdown',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Shutdown',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_shutdown',
'unique_id': 'SS2ES104N5S445_fault_shutdown',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_shutdown-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Shutdown',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_shutdown',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_string_fuse_open-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_string_fuse_open',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'String fuse open',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'String fuse open',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_string_fuse',
'unique_id': 'SS2ES104N5S445_fault_string_fuse',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_string_fuse_open-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 String fuse open',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_string_fuse_open',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_thermal_fault-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_thermal_fault',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Thermal fault',
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Thermal fault',
'platform': 'sofar',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'fault_thermal',
'unique_id': 'SS2ES104N5S445_fault_thermal',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[binary_sensor.4_4_ktlx_g3_thermal_fault-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'problem',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: '4.4 KTLX-G3 Thermal fault',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.4_4_ktlx_g3_thermal_fault',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
@@ -1,6 +1,8 @@
# serializer version: 1
# name: test_diagnostics
dict({
'active_faults': list([
]),
'inverter_type': 1537,
'model': '4.4 KTLX-G3',
'raw': dict({
@@ -0,0 +1,124 @@
"""Test the Sofar Inverter Modbus binary sensor platform."""
from unittest.mock import patch
from modbus_connection.mock import MockModbusConnection
import pytest
from sofar_modbus.modern.faults import FaultCategory
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.sofar.binary_sensor import FAULT_SENSOR_DESCRIPTIONS
from homeassistant.components.sofar.const import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MOCK_MODEL, MOCK_SERIAL, MOCK_USER_INPUT, seed_pv_inverter
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities match their snapshot on a PV device."""
connection = MockModbusConnection()
seed_pv_inverter(connection.for_unit(1))
entry = MockConfigEntry(
domain=DOMAIN, unique_id=MOCK_SERIAL, data=MOCK_USER_INPUT, title=MOCK_MODEL
)
entry.add_to_hass(hass)
with (
patch("homeassistant.components.sofar.PLATFORMS", [Platform.BINARY_SENSOR]),
patch(
"homeassistant.components.sofar.async_get_unit",
side_effect=lambda hass, entry, params, unit_id: connection.for_unit(
unit_id
),
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
async def test_no_faults_reports_off(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
) -> None:
"""Test every category is off when nothing is seeded."""
entity_id = entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_fault_grid"
)
assert entity_id is not None
assert (state := hass.states.get(entity_id)) is not None
assert state.state == STATE_OFF
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_one_bit_only_lights_its_own_category(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_connection: MockModbusConnection,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test a fan failure isn't masked by a shutdown bit in the same register."""
unit = mock_connection.for_unit(1)
unit.holding[0x0405] = 0b1 # ID001_GRID_OVER_VOLTAGE
unit.holding[0x040F] = 0b100000001 # ID161_FORCED_SHUTDOWN | ID169_FAN_1_FAILURE
unit.holding[0x0410] = 0b100 # ID179_BMS_HIGH_TEMPERATURE_PROTECTION
with patch(
"homeassistant.components.sofar.async_get_unit",
side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit(
unit_id
),
):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
on_categories = {"grid", "shutdown", "fan", "battery"}
for description in FAULT_SENSOR_DESCRIPTIONS:
entity_id = entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_{description.key}"
)
assert entity_id is not None
assert (state := hass.states.get(entity_id)) is not None
expected = (
STATE_ON if description.category.value in on_categories else STATE_OFF
)
assert state.state == expected, description.category
async def test_enabled_by_default_excludes_commercial_hardware(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
) -> None:
"""Test combiner box, string fuse, input fuse and AFCI start disabled."""
entries = [
e
for e in er.async_entries_for_config_entry(
entity_registry, init_integration.entry_id
)
if e.domain == BINARY_SENSOR_DOMAIN
]
assert len(entries) == len(FAULT_SENSOR_DESCRIPTIONS)
disabled_categories = {
FaultCategory(e.unique_id.removeprefix(f"{MOCK_SERIAL}_fault_"))
for e in entries
if e.disabled_by is er.RegistryEntryDisabler.INTEGRATION
}
assert disabled_categories == {
FaultCategory.ARC_FAULT,
FaultCategory.COMBINER_BOX,
FaultCategory.INPUT_FUSE,
FaultCategory.STRING_FUSE,
}
@@ -1,5 +1,8 @@
"""Test the Sofar Inverter Modbus diagnostics."""
from unittest.mock import patch
from modbus_connection.mock import MockModbusConnection
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
@@ -21,6 +24,30 @@ async def test_diagnostics(
assert diag == snapshot
async def test_diagnostics_includes_active_faults(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_connection: MockModbusConnection,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test active faults are decoded by name in the diagnostics dump."""
mock_connection.for_unit(1).holding[0x0405] = 0b1 # ID001_GRID_OVER_VOLTAGE
with patch(
"homeassistant.components.sofar.async_get_unit",
side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit(
unit_id
),
):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
diag = await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
assert diag["active_faults"] == ["grid_over_voltage"]
async def test_diagnostics_redacts_serial_number(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,