diff --git a/homeassistant/components/indevolt/__init__.py b/homeassistant/components/indevolt/__init__.py index 3e8d8b7177b91..20d8d692ad40a 100644 --- a/homeassistant/components/indevolt/__init__.py +++ b/homeassistant/components/indevolt/__init__.py @@ -12,6 +12,7 @@ from .services import async_setup_services PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.BUTTON, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py new file mode 100644 index 0000000000000..109a9488f6bbd --- /dev/null +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -0,0 +1,154 @@ +"""Binary sensor platform for Indevolt integration.""" + +from dataclasses import dataclass +from typing import Final + +from indevolt_api import IndevoltBattery, IndevoltGrid, IndevoltSystem + +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 . import IndevoltConfigEntry +from .coordinator import IndevoltCoordinator +from .entity import IndevoltEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): + """Custom entity description class for Indevolt binary sensors.""" + + on_value: int = 1 + off_value: int = 0 + generation: tuple[int, ...] = (1, 2) + + +BINARY_SENSORS: Final = ( + # Electricity Meter Status + IndevoltBinarySensorEntityDescription( + key=IndevoltGrid.METER_CONNECTED, + translation_key="meter_connected", + on_value=1000, + off_value=1001, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Electric Heating States + IndevoltBinarySensorEntityDescription( + key=IndevoltSystem.HEATING_STATE, + generation=(1,), + translation_key="electric_heating_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key=IndevoltBattery.MAIN_HEATING_STATE, + generation=(2,), + translation_key="main_electric_heating_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key=IndevoltBattery.PACK_1_HEATING_STATE, + generation=(2,), + translation_key="battery_pack_1_electric_heating_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key=IndevoltBattery.PACK_2_HEATING_STATE, + generation=(2,), + translation_key="battery_pack_2_electric_heating_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key=IndevoltBattery.PACK_3_HEATING_STATE, + generation=(2,), + translation_key="battery_pack_3_electric_heating_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key=IndevoltBattery.PACK_4_HEATING_STATE, + generation=(2,), + translation_key="battery_pack_4_electric_heating_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key=IndevoltBattery.PACK_5_HEATING_STATE, + generation=(2,), + translation_key="battery_pack_5_electric_heating_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +) + +# Sensor per battery pack: (serial_number_key, heating_state_key) +BATTERY_PACK_SENSOR_KEYS = [ + (IndevoltBattery.PACK_1_SERIAL_NUMBER, IndevoltBattery.PACK_1_HEATING_STATE), + (IndevoltBattery.PACK_2_SERIAL_NUMBER, IndevoltBattery.PACK_2_HEATING_STATE), + (IndevoltBattery.PACK_3_SERIAL_NUMBER, IndevoltBattery.PACK_3_HEATING_STATE), + (IndevoltBattery.PACK_4_SERIAL_NUMBER, IndevoltBattery.PACK_4_HEATING_STATE), + (IndevoltBattery.PACK_5_SERIAL_NUMBER, IndevoltBattery.PACK_5_HEATING_STATE), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: IndevoltConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the binary sensor platform for Indevolt.""" + coordinator = entry.runtime_data + device_gen = coordinator.generation + + excluded_keys: set[str] = set() + for sn_key, heating_key in BATTERY_PACK_SENSOR_KEYS: + if not coordinator.data.get(sn_key): + excluded_keys.add(heating_key) + + async_add_entities( + IndevoltBinarySensorEntity(coordinator, description) + for description in BINARY_SENSORS + if device_gen in description.generation and description.key not in excluded_keys + ) + + +class IndevoltBinarySensorEntity(IndevoltEntity, BinarySensorEntity): + """Represents a binary sensor entity for Indevolt devices.""" + + entity_description: IndevoltBinarySensorEntityDescription + + def __init__( + self, + coordinator: IndevoltCoordinator, + description: IndevoltBinarySensorEntityDescription, + ) -> None: + """Initialize the Indevolt binary sensor entity.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{self.serial_number}_{description.key}" + + @property + def is_on(self) -> bool | None: + """Return on/active state of the binary sensor.""" + raw_value = self.coordinator.data.get(self.entity_description.key) + + if raw_value == self.entity_description.on_value: + return True + + if raw_value == self.entity_description.off_value: + return False + + return None diff --git a/homeassistant/components/indevolt/const.py b/homeassistant/components/indevolt/const.py index 6c41a4d874c19..5255d4595fa42 100644 --- a/homeassistant/components/indevolt/const.py +++ b/homeassistant/components/indevolt/const.py @@ -38,7 +38,9 @@ IndevoltSolar.DC_INPUT_POWER_4, IndevoltConfig.READ_DISCHARGE_LIMIT, IndevoltGrid.METER_POWER_GEN1, + IndevoltGrid.METER_CONNECTED, IndevoltSolar.CUMULATIVE_PRODUCTION, + IndevoltSystem.HEATING_STATE, ], 2: [ IndevoltSystem.OPERATING_MODE, @@ -112,6 +114,13 @@ IndevoltConfig.READ_INVERTER_INPUT_LIMIT, IndevoltConfig.READ_FEEDIN_POWER_LIMIT, IndevoltConfig.READ_DISCHARGE_LIMIT, + IndevoltBattery.MAIN_HEATING_STATE, + IndevoltBattery.PACK_1_HEATING_STATE, + IndevoltBattery.PACK_2_HEATING_STATE, + IndevoltBattery.PACK_3_HEATING_STATE, + IndevoltBattery.PACK_4_HEATING_STATE, + IndevoltBattery.PACK_5_HEATING_STATE, + IndevoltGrid.METER_CONNECTED, IndevoltSolar.CUMULATIVE_PRODUCTION, ], } diff --git a/homeassistant/components/indevolt/strings.json b/homeassistant/components/indevolt/strings.json index 44de0f565201f..b64c1b9047889 100644 --- a/homeassistant/components/indevolt/strings.json +++ b/homeassistant/components/indevolt/strings.json @@ -35,6 +35,32 @@ } }, "entity": { + "binary_sensor": { + "battery_pack_1_electric_heating_state": { + "name": "Battery pack 1 electric heating" + }, + "battery_pack_2_electric_heating_state": { + "name": "Battery pack 2 electric heating" + }, + "battery_pack_3_electric_heating_state": { + "name": "Battery pack 3 electric heating" + }, + "battery_pack_4_electric_heating_state": { + "name": "Battery pack 4 electric heating" + }, + "battery_pack_5_electric_heating_state": { + "name": "Battery pack 5 electric heating" + }, + "electric_heating_state": { + "name": "Electric heating" + }, + "main_electric_heating_state": { + "name": "Main electric heating" + }, + "meter_connected": { + "name": "Meter connected" + } + }, "button": { "stop": { "name": "Enable standby mode" diff --git a/tests/components/indevolt/fixtures/gen_1.json b/tests/components/indevolt/fixtures/gen_1.json index 46269f8396f25..9cf4dd98e344b 100644 --- a/tests/components/indevolt/fixtures/gen_1.json +++ b/tests/components/indevolt/fixtures/gen_1.json @@ -18,6 +18,7 @@ "6005": 0, "6006": 277.16, "6007": 256.39, - "7120": 1001, + "7120": 1000, + "7121": 1, "21028": 0 } diff --git a/tests/components/indevolt/fixtures/gen_2.json b/tests/components/indevolt/fixtures/gen_2.json index e267a9aafb112..90cf1aa987c42 100644 --- a/tests/components/indevolt/fixtures/gen_2.json +++ b/tests/components/indevolt/fixtures/gen_2.json @@ -21,6 +21,12 @@ "6006": 380.58, "6007": 338.07, "7120": 1001, + "9079": 1, + "9096": 1, + "9112": 0, + "9128": 1, + "9144": 0, + "9279": 1, "11016": 0, "2600": 1200, "2612": 50.0, diff --git a/tests/components/indevolt/snapshots/test_binary_sensor.ambr b/tests/components/indevolt/snapshots/test_binary_sensor.ambr new file mode 100644 index 0000000000000..c122ef7f1da72 --- /dev/null +++ b/tests/components/indevolt/snapshots/test_binary_sensor.ambr @@ -0,0 +1,453 @@ +# serializer version: 1 +# name: test_binary_sensor[1][binary_sensor.bk1600_electric_heating-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bk1600_electric_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Electric heating', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Electric heating', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'electric_heating_state', + 'unique_id': 'BK1600-12345678_7121', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[1][binary_sensor.bk1600_electric_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'BK1600 Electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.bk1600_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[1][binary_sensor.bk1600_meter_connected-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bk1600_meter_connected', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Meter connected', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Meter connected', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'meter_connected', + 'unique_id': 'BK1600-12345678_7120', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[1][binary_sensor.bk1600_meter_connected-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'BK1600 Meter connected', + }), + 'context': , + 'entity_id': 'binary_sensor.bk1600_meter_connected', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_1_electric_heating-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_1_electric_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 1 electric heating', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 1 electric heating', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_1_electric_heating_state', + 'unique_id': 'SolidFlex2000-87654321_9096', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_1_electric_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 1 electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_1_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_2_electric_heating-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_2_electric_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 2 electric heating', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 2 electric heating', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_2_electric_heating_state', + 'unique_id': 'SolidFlex2000-87654321_9112', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_2_electric_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 2 electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_2_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_3_electric_heating-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_3_electric_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 3 electric heating', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 3 electric heating', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_3_electric_heating_state', + 'unique_id': 'SolidFlex2000-87654321_9128', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_3_electric_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 3 electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_3_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_4_electric_heating-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_4_electric_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 4 electric heating', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 4 electric heating', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_4_electric_heating_state', + 'unique_id': 'SolidFlex2000-87654321_9144', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_4_electric_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 4 electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_4_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_5_electric_heating-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_5_electric_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack 5 electric heating', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery pack 5 electric heating', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_pack_5_electric_heating_state', + 'unique_id': 'SolidFlex2000-87654321_9279', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_5_electric_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Battery pack 5 electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_battery_pack_5_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_main_electric_heating-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cms_sf2000_main_electric_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Main electric heating', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Main electric heating', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'main_electric_heating_state', + 'unique_id': 'SolidFlex2000-87654321_9079', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_main_electric_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CMS-SF2000 Main electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_main_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_meter_connected-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': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.cms_sf2000_meter_connected', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Meter connected', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Meter connected', + 'platform': 'indevolt', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'meter_connected', + 'unique_id': 'SolidFlex2000-87654321_7120', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_meter_connected-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'CMS-SF2000 Meter connected', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_meter_connected', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/indevolt/snapshots/test_diagnostics.ambr b/tests/components/indevolt/snapshots/test_diagnostics.ambr index 017ebe8b43ba3..f540804a7116d 100644 --- a/tests/components/indevolt/snapshots/test_diagnostics.ambr +++ b/tests/components/indevolt/snapshots/test_diagnostics.ambr @@ -22,7 +22,8 @@ '606': '1000', '6105': 5, '7101': 5, - '7120': 1001, + '7120': 1000, + '7121': 1, }), 'device': dict({ 'firmware_version': '1.2.3', @@ -109,6 +110,11 @@ '9058': 51.1, '9068': 25.0, '9070': '**REDACTED**', + '9079': 1, + '9096': 1, + '9112': 0, + '9128': 1, + '9144': 0, '9149': 94, '9153': 51.4, '9163': 25.7, @@ -117,6 +123,7 @@ '9206': 50.9, '9216': 24.9, '9218': '**REDACTED**', + '9279': 1, }), 'device': dict({ 'firmware_version': '1.2.3', diff --git a/tests/components/indevolt/test_binary_sensor.py b/tests/components/indevolt/test_binary_sensor.py new file mode 100644 index 0000000000000..d88320a139c2d --- /dev/null +++ b/tests/components/indevolt/test_binary_sensor.py @@ -0,0 +1,149 @@ +"""Tests for the Indevolt binary sensor platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.indevolt.coordinator import SCAN_INTERVAL +from homeassistant.const import ( + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +METER_CONNECTED_KEY = "7120" + +METER_CONNECTED_VALUE = 1000 +METER_DISCONNECTED_VALUE = 1001 + +ENTITY_ID_GEN2 = "binary_sensor.cms_sf2000_meter_connected" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("generation", [1, 2], indirect=True) +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_indevolt: AsyncMock, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test binary sensor entity registration and states.""" + with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("generation", [2], indirect=True) +async def test_meter_connected_state_changes( + hass: HomeAssistant, + mock_indevolt: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test meter_connected state transitions between ON, OFF, and unknown.""" + + # Setup integration: initial value is OFF (1001) + mock_indevolt.fetch_data.return_value[METER_CONNECTED_KEY] = ( + METER_DISCONNECTED_VALUE + ) + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None + assert state.state == STATE_OFF + + # Simulate coordinator update: value changes to ON (1000) + mock_indevolt.fetch_data.return_value[METER_CONNECTED_KEY] = METER_CONNECTED_VALUE + freezer.tick(delta=timedelta(seconds=SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None + assert state.state == STATE_ON + + # Simulate coordinator update: value is unknown (neither ON nor OFF) + mock_indevolt.fetch_data.return_value[METER_CONNECTED_KEY] = None + freezer.tick(delta=timedelta(seconds=SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None + assert state.state == STATE_UNKNOWN + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("generation", [2], indirect=True) +async def test_binary_sensor_availability( + hass: HomeAssistant, + mock_indevolt: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor availability when coordinator fails.""" + await setup_integration(hass, mock_config_entry) + + # Simulate connection error + mock_indevolt.fetch_data.side_effect = ConnectionError + freezer.tick(delta=timedelta(seconds=SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("generation", [2], indirect=True) +async def test_battery_pack_heating_filtering( + hass: HomeAssistant, + mock_indevolt: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that battery pack sensors are filtered based on SN availability.""" + + # Mock battery pack data - only first two packs have SNs + mock_indevolt.fetch_data.return_value = { + "9032": "BAT001", + "9051": "BAT002", + "9070": None, + "9165": "", + "9218": None, + } + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Get all binary sensor entities + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + # Verify sensors for packs 1 and 2 exist (with SNs) + pack1_sensors = [e for e in entity_entries if e.unique_id.endswith("9096")] + pack2_sensors = [e for e in entity_entries if e.unique_id.endswith("9112")] + + assert len(pack1_sensors) == 1 + assert len(pack2_sensors) == 1 + + # Verify sensors for packs 3, 4, and 5 don't exist (no SNs) + pack3_sensors = [e for e in entity_entries if e.unique_id.endswith("9128")] + pack4_sensors = [e for e in entity_entries if e.unique_id.endswith("9144")] + pack5_sensors = [e for e in entity_entries if e.unique_id.endswith("9279")] + + assert len(pack3_sensors) == 0 + assert len(pack4_sensors) == 0 + assert len(pack5_sensors) == 0