From d2c6c383d982bded46f10cbc05272f12292e153e Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Mon, 23 Mar 2026 22:55:34 +0000 Subject: [PATCH 01/15] Add binary sensor platform to Indevolt integration --- homeassistant/components/indevolt/__init__.py | 1 + .../components/indevolt/binary_sensor.py | 147 +++++++ homeassistant/components/indevolt/const.py | 8 + .../components/indevolt/strings.json | 23 + .../snapshots/test_binary_sensor.ambr | 401 ++++++++++++++++++ .../components/indevolt/test_binary_sensor.py | 149 +++++++ 6 files changed, 729 insertions(+) create mode 100644 homeassistant/components/indevolt/binary_sensor.py create mode 100644 tests/components/indevolt/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/indevolt/test_binary_sensor.py diff --git a/homeassistant/components/indevolt/__init__.py b/homeassistant/components/indevolt/__init__.py index d2a911a1564e2..8eb638b894573 100644 --- a/homeassistant/components/indevolt/__init__.py +++ b/homeassistant/components/indevolt/__init__.py @@ -8,6 +8,7 @@ from .coordinator import IndevoltConfigEntry, IndevoltCoordinator PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py new file mode 100644 index 0000000000000..e2b6568aa6e42 --- /dev/null +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -0,0 +1,147 @@ +"""Binary sensor platform for Indevolt integration.""" + +from dataclasses import dataclass, field +from typing import Final + +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 + generation: list[int] = field(default_factory=lambda: [1, 2]) + + +BINARY_SENSORS: Final = ( + # Electricity Meter Status + IndevoltBinarySensorEntityDescription( + key="7120", + translation_key="meter_connected", + on_value=1000, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Electric Heating State + IndevoltBinarySensorEntityDescription( + key="9079", + generation=[2], + translation_key="main_electric_heating_state", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key="9096", + generation=[2], + translation_key="battery_pack_1_electric_heating_state", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key="9112", + generation=[2], + translation_key="battery_pack_2_electric_heating_state", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key="9128", + generation=[2], + translation_key="battery_pack_3_electric_heating_state", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key="9144", + generation=[2], + translation_key="battery_pack_4_electric_heating_state", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + IndevoltBinarySensorEntityDescription( + key="9279", + generation=[2], + translation_key="battery_pack_5_electric_heating_state", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +) + +# Sensors per battery pack (SN, heating state) +BATTERY_PACK_SENSOR_KEYS = [ + ("9032", "9096"), # Battery Pack 1 + ("9051", "9112"), # Battery Pack 2 + ("9070", "9128"), # Battery Pack 3 + ("9165", "9144"), # Battery Pack 4 + ("9218", "9279"), # Battery Pack 5 +] + + +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 pack_keys in BATTERY_PACK_SENSOR_KEYS: + sn_key = pack_keys[0] + + if not coordinator.data.get(sn_key): + excluded_keys.update(pack_keys) + + 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 is None: + return None + + return raw_value == self.entity_description.on_value diff --git a/homeassistant/components/indevolt/const.py b/homeassistant/components/indevolt/const.py index 17d857dee51b0..ade7a6c00f7de 100644 --- a/homeassistant/components/indevolt/const.py +++ b/homeassistant/components/indevolt/const.py @@ -28,6 +28,7 @@ "1667", "6105", "21028", + "7120", "1505", ], 2: [ @@ -102,6 +103,13 @@ "11009", "11010", "6105", + "9079", + "9096", + "9112", + "9128", + "9144", + "9279", + "7120", "1505", ], } diff --git a/homeassistant/components/indevolt/strings.json b/homeassistant/components/indevolt/strings.json index ccbdffa80e878..87ab66e6c4d88 100644 --- a/homeassistant/components/indevolt/strings.json +++ b/homeassistant/components/indevolt/strings.json @@ -35,6 +35,29 @@ } }, "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" + }, + "main_electric_heating_state": { + "name": "Main electric heating" + }, + "meter_connected": { + "name": "Meter connected" + } + }, "number": { "discharge_limit": { "name": "Discharge limit" 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..9a21bb5aa57d7 --- /dev/null +++ b/tests/components/indevolt/snapshots/test_binary_sensor.ambr @@ -0,0 +1,401 @@ +# serializer version: 1 +# name: test_binary_sensor[1][binary_sensor.bk1600_meter_connected-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': 'off', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_1_electric_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': , + '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({ + 'device_class': 'heat', + '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': 'unknown', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_2_electric_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': , + '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({ + 'device_class': 'heat', + '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': 'unknown', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_3_electric_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': , + '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({ + 'device_class': 'heat', + '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': 'unknown', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_4_electric_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': , + '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({ + 'device_class': 'heat', + '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': 'unknown', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_5_electric_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': , + '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({ + 'device_class': 'heat', + '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': 'unknown', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_main_electric_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': , + '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({ + 'device_class': 'heat', + 'friendly_name': 'CMS-SF2000 Main electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.cms_sf2000_main_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[2][binary_sensor.cms_sf2000_meter_connected-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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/test_binary_sensor.py b/tests/components/indevolt/test_binary_sensor.py new file mode 100644 index 0000000000000..6dcf495404afd --- /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, 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" +ENTITY_ID_GEN1 = "binary_sensor.bk1600_meter_connected" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("generation", [2, 1], 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", [1], indirect=True) +async def test_meter_connected_on_state( + hass: HomeAssistant, + mock_indevolt: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test meter_connected reports ON when value is 1000 (Enable).""" + mock_indevolt.fetch_data.return_value[METER_CONNECTED_KEY] = METER_CONNECTED_VALUE + + with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + # Verify updated state (available) + assert (state := hass.states.get(ENTITY_ID_GEN1)) is not None + assert state.state == STATE_ON + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("generation", [2], indirect=True) +async def test_meter_connected_off_state( + hass: HomeAssistant, + mock_indevolt: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test meter_connected reports OFF when value is 1001 (Disable).""" + mock_indevolt.fetch_data.return_value[METER_CONNECTED_KEY] = ( + METER_DISCONNECTED_VALUE + ) + + with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + # Verify updated state (unavailable) + assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None + assert state.state == STATE_OFF + + +@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.""" + with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + # Verify initial state (available) + assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None + assert state.state == STATE_OFF + + # Simulate a fetch 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() + + # Verify updated state (unavailable) + 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 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 "9096" in e.unique_id] + pack2_sensors = [e for e in entity_entries if "9112" in e.unique_id] + + 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 "9128" in e.unique_id] + pack4_sensors = [e for e in entity_entries if "9144" in e.unique_id] + pack5_sensors = [e for e in entity_entries if "9279" in e.unique_id] + + assert len(pack3_sensors) == 0 + assert len(pack4_sensors) == 0 + assert len(pack5_sensors) == 0 From c6138fa87b2344690cc67546315a3197865ca481 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Sun, 26 Apr 2026 20:52:14 +0000 Subject: [PATCH 02/15] Updated to use API library 1.6.0 --- .../components/indevolt/binary_sensor.py | 32 +-- homeassistant/components/indevolt/const.py | 202 +++++++++--------- .../components/indevolt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 125 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py index e2b6568aa6e42..a15570f05da10 100644 --- a/homeassistant/components/indevolt/binary_sensor.py +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -3,6 +3,8 @@ from dataclasses import dataclass, field from typing import Final +from indevolt_api import IndevoltBattery, IndevoltGrid + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -30,7 +32,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): BINARY_SENSORS: Final = ( # Electricity Meter Status IndevoltBinarySensorEntityDescription( - key="7120", + key=IndevoltGrid.METER_CONNECTED, translation_key="meter_connected", on_value=1000, device_class=BinarySensorDeviceClass.CONNECTIVITY, @@ -39,7 +41,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): ), # Electric Heating State IndevoltBinarySensorEntityDescription( - key="9079", + key=IndevoltBattery.MAIN_HEATING_STATE, generation=[2], translation_key="main_electric_heating_state", device_class=BinarySensorDeviceClass.HEAT, @@ -47,7 +49,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): entity_registry_enabled_default=False, ), IndevoltBinarySensorEntityDescription( - key="9096", + key=IndevoltBattery.PACK_1_HEATING_STATE, generation=[2], translation_key="battery_pack_1_electric_heating_state", device_class=BinarySensorDeviceClass.HEAT, @@ -55,7 +57,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): entity_registry_enabled_default=False, ), IndevoltBinarySensorEntityDescription( - key="9112", + key=IndevoltBattery.PACK_2_HEATING_STATE, generation=[2], translation_key="battery_pack_2_electric_heating_state", device_class=BinarySensorDeviceClass.HEAT, @@ -63,7 +65,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): entity_registry_enabled_default=False, ), IndevoltBinarySensorEntityDescription( - key="9128", + key=IndevoltBattery.PACK_3_HEATING_STATE, generation=[2], translation_key="battery_pack_3_electric_heating_state", device_class=BinarySensorDeviceClass.HEAT, @@ -71,7 +73,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): entity_registry_enabled_default=False, ), IndevoltBinarySensorEntityDescription( - key="9144", + key=IndevoltBattery.PACK_4_HEATING_STATE, generation=[2], translation_key="battery_pack_4_electric_heating_state", device_class=BinarySensorDeviceClass.HEAT, @@ -79,7 +81,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): entity_registry_enabled_default=False, ), IndevoltBinarySensorEntityDescription( - key="9279", + key=IndevoltBattery.PACK_5_HEATING_STATE, generation=[2], translation_key="battery_pack_5_electric_heating_state", device_class=BinarySensorDeviceClass.HEAT, @@ -88,13 +90,13 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): ), ) -# Sensors per battery pack (SN, heating state) +# Sensor per battery pack (heating state) BATTERY_PACK_SENSOR_KEYS = [ - ("9032", "9096"), # Battery Pack 1 - ("9051", "9112"), # Battery Pack 2 - ("9070", "9128"), # Battery Pack 3 - ("9165", "9144"), # Battery Pack 4 - ("9218", "9279"), # Battery Pack 5 + (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), ] @@ -109,9 +111,9 @@ async def async_setup_entry( excluded_keys: set[str] = set() for pack_keys in BATTERY_PACK_SENSOR_KEYS: - sn_key = pack_keys[0] + first_key = pack_keys[0] - if not coordinator.data.get(sn_key): + if not coordinator.data.get(first_key): excluded_keys.update(pack_keys) async_add_entities( diff --git a/homeassistant/components/indevolt/const.py b/homeassistant/components/indevolt/const.py index ade7a6c00f7de..5f20515be4463 100644 --- a/homeassistant/components/indevolt/const.py +++ b/homeassistant/components/indevolt/const.py @@ -1,5 +1,13 @@ """Constants for the Indevolt integration.""" +from indevolt_api import ( + IndevoltBattery, + IndevoltConfig, + IndevoltGrid, + IndevoltSolar, + IndevoltSystem, +) + DOMAIN = "indevolt" # Config entry fields @@ -12,104 +20,104 @@ # API key fields SENSOR_KEYS = { 1: [ - "606", - "7101", - "2101", - "2108", - "2107", - "6000", - "6001", - "6002", - "1501", - "1502", - "1664", - "1665", - "1666", - "1667", - "6105", - "21028", - "7120", - "1505", + IndevoltSystem.OPERATING_MODE, + IndevoltConfig.READ_ENERGY_MODE, + IndevoltSystem.INPUT_POWER, + IndevoltSystem.OUTPUT_POWER, + IndevoltSystem.TOTAL_INPUT_ENERGY, + IndevoltBattery.POWER, + IndevoltBattery.CHARGE_DISCHARGE_STATE, + IndevoltBattery.SOC, + IndevoltSolar.DC_OUTPUT_POWER, + IndevoltSolar.DAILY_PRODUCTION, + IndevoltSolar.DC_INPUT_POWER_1, + IndevoltSolar.DC_INPUT_POWER_2, + IndevoltSolar.DC_INPUT_POWER_3, + IndevoltSolar.DC_INPUT_POWER_4, + IndevoltConfig.READ_DISCHARGE_LIMIT, + IndevoltGrid.METER_POWER_GEN1, + IndevoltGrid.METER_CONNECTED, + IndevoltSolar.CUMULATIVE_PRODUCTION, ], 2: [ - "606", - "7101", - "2101", - "2108", - "2107", - "6000", - "6001", - "6002", - "1501", - "1502", - "1664", - "1665", - "1666", - "1667", - "142", - "667", - "2104", - "2105", - "11034", - "6004", - "6005", - "6006", - "6007", - "11016", - "2600", - "2612", - "1632", - "1600", - "1633", - "1601", - "1634", - "1602", - "1635", - "1603", - "9008", - "9032", - "9051", - "9070", - "9165", - "9218", - "9000", - "9016", - "9035", - "9054", - "9149", - "9202", - "9012", - "9030", - "9049", - "9068", - "9163", - "9216", - "9004", - "9020", - "9039", - "9058", - "9153", - "9206", - "9013", - "19173", - "19174", - "19175", - "19176", - "19177", - "680", - "2618", - "7171", - "11011", - "11009", - "11010", - "6105", - "9079", - "9096", - "9112", - "9128", - "9144", - "9279", - "7120", - "1505", + IndevoltSystem.OPERATING_MODE, + IndevoltConfig.READ_ENERGY_MODE, + IndevoltSystem.INPUT_POWER, + IndevoltSystem.OUTPUT_POWER, + IndevoltSystem.TOTAL_INPUT_ENERGY, + IndevoltBattery.POWER, + IndevoltBattery.CHARGE_DISCHARGE_STATE, + IndevoltBattery.SOC, + IndevoltSolar.DC_OUTPUT_POWER, + IndevoltSolar.DAILY_PRODUCTION, + IndevoltSolar.DC_INPUT_POWER_1, + IndevoltSolar.DC_INPUT_POWER_2, + IndevoltSolar.DC_INPUT_POWER_3, + IndevoltSolar.DC_INPUT_POWER_4, + IndevoltBattery.RATED_CAPACITY_GEN2, + IndevoltSystem.BYPASS_POWER, + IndevoltSystem.TOTAL_OUTPUT_ENERGY, + IndevoltSystem.OFF_GRID_OUTPUT_ENERGY, + IndevoltSystem.BYPASS_INPUT_ENERGY, + IndevoltBattery.DAILY_CHARGING_ENERGY, + IndevoltBattery.DAILY_DISCHARGING_ENERGY, + IndevoltBattery.TOTAL_CHARGING_ENERGY, + IndevoltBattery.TOTAL_DISCHARGING_ENERGY, + IndevoltGrid.METER_POWER_GEN2, + IndevoltGrid.VOLTAGE, + IndevoltGrid.FREQUENCY, + IndevoltSolar.DC_INPUT_CURRENT_1, + IndevoltSolar.DC_INPUT_VOLTAGE_1, + IndevoltSolar.DC_INPUT_CURRENT_2, + IndevoltSolar.DC_INPUT_VOLTAGE_2, + IndevoltSolar.DC_INPUT_CURRENT_3, + IndevoltSolar.DC_INPUT_VOLTAGE_3, + IndevoltSolar.DC_INPUT_CURRENT_4, + IndevoltSolar.DC_INPUT_VOLTAGE_4, + IndevoltBattery.MAIN_SERIAL_NUMBER, + IndevoltBattery.PACK_1_SERIAL_NUMBER, + IndevoltBattery.PACK_2_SERIAL_NUMBER, + IndevoltBattery.PACK_3_SERIAL_NUMBER, + IndevoltBattery.PACK_4_SERIAL_NUMBER, + IndevoltBattery.PACK_5_SERIAL_NUMBER, + IndevoltBattery.MAIN_SOC, + IndevoltBattery.PACK_1_SOC, + IndevoltBattery.PACK_2_SOC, + IndevoltBattery.PACK_3_SOC, + IndevoltBattery.PACK_4_SOC, + IndevoltBattery.PACK_5_SOC, + IndevoltBattery.MAIN_TEMPERATURE, + IndevoltBattery.PACK_1_TEMPERATURE, + IndevoltBattery.PACK_2_TEMPERATURE, + IndevoltBattery.PACK_3_TEMPERATURE, + IndevoltBattery.PACK_4_TEMPERATURE, + IndevoltBattery.PACK_5_TEMPERATURE, + IndevoltBattery.MAIN_VOLTAGE, + IndevoltBattery.PACK_1_VOLTAGE, + IndevoltBattery.PACK_2_VOLTAGE, + IndevoltBattery.PACK_3_VOLTAGE, + IndevoltBattery.PACK_4_VOLTAGE, + IndevoltBattery.PACK_5_VOLTAGE, + IndevoltBattery.MAIN_CURRENT, + IndevoltBattery.PACK_1_CURRENT, + IndevoltBattery.PACK_2_CURRENT, + IndevoltBattery.PACK_3_CURRENT, + IndevoltBattery.PACK_4_CURRENT, + IndevoltBattery.PACK_5_CURRENT, + IndevoltConfig.READ_BYPASS, + IndevoltConfig.READ_GRID_CHARGING, + IndevoltConfig.READ_LIGHT, + IndevoltConfig.READ_MAX_AC_OUTPUT_POWER, + 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/manifest.json b/homeassistant/components/indevolt/manifest.json index 2e67b487bd60d..78d72911e60f4 100644 --- a/homeassistant/components/indevolt/manifest.json +++ b/homeassistant/components/indevolt/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["indevolt-api==1.2.3"] + "requirements": ["indevolt-api==1.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9cb7b0b2ea946..4bd4b4ee98582 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ imgw_pib==2.0.2 incomfort-client==0.6.12 # homeassistant.components.indevolt -indevolt-api==1.2.3 +indevolt-api==1.6.0 # homeassistant.components.influxdb influxdb-client==1.50.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7d3aaa3e74ab..34d4c908aeaa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1153,7 +1153,7 @@ imgw_pib==2.0.2 incomfort-client==0.6.12 # homeassistant.components.indevolt -indevolt-api==1.2.3 +indevolt-api==1.6.0 # homeassistant.components.influxdb influxdb-client==1.50.0 From 8bd828878bb63f142a73e95d29a6312f8e148db4 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Mon, 27 Apr 2026 21:47:28 +0000 Subject: [PATCH 03/15] Bump indevolt-api to 1.6.3 --- homeassistant/components/indevolt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/indevolt/manifest.json b/homeassistant/components/indevolt/manifest.json index 78d72911e60f4..7760d372e0d2b 100644 --- a/homeassistant/components/indevolt/manifest.json +++ b/homeassistant/components/indevolt/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["indevolt-api==1.6.0"] + "requirements": ["indevolt-api==1.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4bd4b4ee98582..ef0a4117bdea5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ imgw_pib==2.0.2 incomfort-client==0.6.12 # homeassistant.components.indevolt -indevolt-api==1.6.0 +indevolt-api==1.6.3 # homeassistant.components.influxdb influxdb-client==1.50.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34d4c908aeaa3..1dde469ca874e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1153,7 +1153,7 @@ imgw_pib==2.0.2 incomfort-client==0.6.12 # homeassistant.components.indevolt -indevolt-api==1.6.0 +indevolt-api==1.6.3 # homeassistant.components.influxdb influxdb-client==1.50.0 From 68c253ae2b25298e90a9b0356b2dc0f446d40eb6 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 13:50:29 +0000 Subject: [PATCH 04/15] Bump indevolt-api to 1.6.4 and add binary sensor platform --- .../components/indevolt/binary_sensor.py | 33 ++++++------ homeassistant/components/indevolt/const.py | 1 + .../components/indevolt/manifest.json | 2 +- .../components/indevolt/strings.json | 3 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_binary_sensor.ambr | 50 +++++++++++++++++++ 7 files changed, 75 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py index a15570f05da10..ed1ed37343421 100644 --- a/homeassistant/components/indevolt/binary_sensor.py +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from typing import Final -from indevolt_api import IndevoltBattery, IndevoltGrid +from indevolt_api import IndevoltBattery, IndevoltGrid, IndevoltSystem from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -40,11 +40,19 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): entity_registry_enabled_default=False, ), # Electric Heating State + IndevoltBinarySensorEntityDescription( + key=IndevoltSystem.HEATING_STATE, + generation=[1], + translation_key="electric_heating_state", + device_class=BinarySensorDeviceClass.HEAT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + # Electric Heating State IndevoltBinarySensorEntityDescription( key=IndevoltBattery.MAIN_HEATING_STATE, generation=[2], translation_key="main_electric_heating_state", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -52,7 +60,6 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): key=IndevoltBattery.PACK_1_HEATING_STATE, generation=[2], translation_key="battery_pack_1_electric_heating_state", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -60,7 +67,6 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): key=IndevoltBattery.PACK_2_HEATING_STATE, generation=[2], translation_key="battery_pack_2_electric_heating_state", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -68,7 +74,6 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): key=IndevoltBattery.PACK_3_HEATING_STATE, generation=[2], translation_key="battery_pack_3_electric_heating_state", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -76,7 +81,6 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): key=IndevoltBattery.PACK_4_HEATING_STATE, generation=[2], translation_key="battery_pack_4_electric_heating_state", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -84,19 +88,18 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): key=IndevoltBattery.PACK_5_HEATING_STATE, generation=[2], translation_key="battery_pack_5_electric_heating_state", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), ) -# Sensor per battery pack (heating state) +# Sensor per battery pack: (serial_number_key, heating_state_key) BATTERY_PACK_SENSOR_KEYS = [ - (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), + (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), ] @@ -111,9 +114,9 @@ async def async_setup_entry( excluded_keys: set[str] = set() for pack_keys in BATTERY_PACK_SENSOR_KEYS: - first_key = pack_keys[0] + sn_key = pack_keys[0] - if not coordinator.data.get(first_key): + if not coordinator.data.get(sn_key): excluded_keys.update(pack_keys) async_add_entities( diff --git a/homeassistant/components/indevolt/const.py b/homeassistant/components/indevolt/const.py index cb49ea67a50fc..5255d4595fa42 100644 --- a/homeassistant/components/indevolt/const.py +++ b/homeassistant/components/indevolt/const.py @@ -40,6 +40,7 @@ IndevoltGrid.METER_POWER_GEN1, IndevoltGrid.METER_CONNECTED, IndevoltSolar.CUMULATIVE_PRODUCTION, + IndevoltSystem.HEATING_STATE, ], 2: [ IndevoltSystem.OPERATING_MODE, diff --git a/homeassistant/components/indevolt/manifest.json b/homeassistant/components/indevolt/manifest.json index 7760d372e0d2b..2f5159f956e7a 100644 --- a/homeassistant/components/indevolt/manifest.json +++ b/homeassistant/components/indevolt/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "bronze", - "requirements": ["indevolt-api==1.6.3"] + "requirements": ["indevolt-api==1.6.4"] } diff --git a/homeassistant/components/indevolt/strings.json b/homeassistant/components/indevolt/strings.json index 19c08bfe35b4b..b64c1b9047889 100644 --- a/homeassistant/components/indevolt/strings.json +++ b/homeassistant/components/indevolt/strings.json @@ -51,6 +51,9 @@ "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" }, diff --git a/requirements_all.txt b/requirements_all.txt index 33aae778be2a9..0163fde2d47d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1329,7 +1329,7 @@ imgw_pib==2.1.1 incomfort-client==0.7.0 # homeassistant.components.indevolt -indevolt-api==1.6.3 +indevolt-api==1.6.4 # homeassistant.components.influxdb influxdb-client==1.50.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99d2c949cd924..970bb7864a9f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1181,7 +1181,7 @@ imgw_pib==2.1.1 incomfort-client==0.7.0 # homeassistant.components.indevolt -indevolt-api==1.6.3 +indevolt-api==1.6.4 # homeassistant.components.influxdb influxdb-client==1.50.0 diff --git a/tests/components/indevolt/snapshots/test_binary_sensor.ambr b/tests/components/indevolt/snapshots/test_binary_sensor.ambr index 9a21bb5aa57d7..9d8bce166813d 100644 --- a/tests/components/indevolt/snapshots/test_binary_sensor.ambr +++ b/tests/components/indevolt/snapshots/test_binary_sensor.ambr @@ -1,4 +1,54 @@ # serializer version: 1 +# name: test_binary_sensor[1][binary_sensor.bk1600_electric_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + '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': , + '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({ + 'device_class': 'heat', + 'friendly_name': 'BK1600 Electric heating', + }), + 'context': , + 'entity_id': 'binary_sensor.bk1600_electric_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor[1][binary_sensor.bk1600_meter_connected-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 502d1b3ed364851df1c3ded7a3bf0e661dff048e Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 14:03:30 +0000 Subject: [PATCH 05/15] Update binary sensor snapshots for upstream entity registry format change --- .../snapshots/test_binary_sensor.ambr | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/tests/components/indevolt/snapshots/test_binary_sensor.ambr b/tests/components/indevolt/snapshots/test_binary_sensor.ambr index 9d8bce166813d..2148939859766 100644 --- a/tests/components/indevolt/snapshots/test_binary_sensor.ambr +++ b/tests/components/indevolt/snapshots/test_binary_sensor.ambr @@ -1,8 +1,9 @@ # serializer version: 1 # name: test_binary_sensor[1][binary_sensor.bk1600_electric_heating-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -51,8 +52,9 @@ # --- # name: test_binary_sensor[1][binary_sensor.bk1600_meter_connected-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -101,8 +103,9 @@ # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_1_electric_heating-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -123,7 +126,7 @@ 'object_id_base': 'Battery pack 1 electric heating', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Battery pack 1 electric heating', 'platform': 'indevolt', @@ -138,7 +141,6 @@ # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_1_electric_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'CMS-SF2000 Battery pack 1 electric heating', }), 'context': , @@ -151,8 +153,9 @@ # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_2_electric_heating-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -173,7 +176,7 @@ 'object_id_base': 'Battery pack 2 electric heating', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Battery pack 2 electric heating', 'platform': 'indevolt', @@ -188,7 +191,6 @@ # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_2_electric_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'CMS-SF2000 Battery pack 2 electric heating', }), 'context': , @@ -201,8 +203,9 @@ # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_3_electric_heating-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -223,7 +226,7 @@ 'object_id_base': 'Battery pack 3 electric heating', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Battery pack 3 electric heating', 'platform': 'indevolt', @@ -238,7 +241,6 @@ # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_3_electric_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'CMS-SF2000 Battery pack 3 electric heating', }), 'context': , @@ -251,8 +253,9 @@ # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_4_electric_heating-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -273,7 +276,7 @@ 'object_id_base': 'Battery pack 4 electric heating', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Battery pack 4 electric heating', 'platform': 'indevolt', @@ -288,7 +291,6 @@ # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_4_electric_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'CMS-SF2000 Battery pack 4 electric heating', }), 'context': , @@ -301,8 +303,9 @@ # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_5_electric_heating-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -323,7 +326,7 @@ 'object_id_base': 'Battery pack 5 electric heating', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Battery pack 5 electric heating', 'platform': 'indevolt', @@ -338,7 +341,6 @@ # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_5_electric_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'CMS-SF2000 Battery pack 5 electric heating', }), 'context': , @@ -351,8 +353,9 @@ # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_main_electric_heating-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , @@ -373,7 +376,7 @@ 'object_id_base': 'Main electric heating', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Main electric heating', 'platform': 'indevolt', @@ -388,7 +391,6 @@ # name: test_binary_sensor[2][binary_sensor.cms_sf2000_main_electric_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'CMS-SF2000 Main electric heating', }), 'context': , @@ -401,8 +403,9 @@ # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_meter_connected-entry] EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), + 'aliases': list([ + None, + ]), 'area_id': None, 'capabilities': None, 'config_entry_id': , From a474afc3c4d4684add7c8d997212ce419f7bf956 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 14:07:17 +0000 Subject: [PATCH 06/15] Remove HEAT device class from gen 1 heating binary sensor --- homeassistant/components/indevolt/binary_sensor.py | 4 +--- tests/components/indevolt/snapshots/test_binary_sensor.ambr | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py index ed1ed37343421..553c10cf02437 100644 --- a/homeassistant/components/indevolt/binary_sensor.py +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -39,16 +39,14 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - # Electric Heating State + # Electric Heating States IndevoltBinarySensorEntityDescription( key=IndevoltSystem.HEATING_STATE, generation=[1], translation_key="electric_heating_state", - device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - # Electric Heating State IndevoltBinarySensorEntityDescription( key=IndevoltBattery.MAIN_HEATING_STATE, generation=[2], diff --git a/tests/components/indevolt/snapshots/test_binary_sensor.ambr b/tests/components/indevolt/snapshots/test_binary_sensor.ambr index 2148939859766..6a99c80861c6b 100644 --- a/tests/components/indevolt/snapshots/test_binary_sensor.ambr +++ b/tests/components/indevolt/snapshots/test_binary_sensor.ambr @@ -24,7 +24,7 @@ 'object_id_base': 'Electric heating', 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Electric heating', 'platform': 'indevolt', @@ -39,7 +39,6 @@ # name: test_binary_sensor[1][binary_sensor.bk1600_electric_heating-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'heat', 'friendly_name': 'BK1600 Electric heating', }), 'context': , From 9515d7d74d7d95a60a99fe85ee3a00c9f3772db6 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 16:25:13 +0200 Subject: [PATCH 07/15] Update tests/components/indevolt/test_binary_sensor.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/components/indevolt/test_binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/indevolt/test_binary_sensor.py b/tests/components/indevolt/test_binary_sensor.py index 6dcf495404afd..28e9c3dab23eb 100644 --- a/tests/components/indevolt/test_binary_sensor.py +++ b/tests/components/indevolt/test_binary_sensor.py @@ -73,7 +73,7 @@ async def test_meter_connected_off_state( with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): await setup_integration(hass, mock_config_entry) - # Verify updated state (unavailable) + # Verify updated state (off) assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None assert state.state == STATE_OFF From 7c2c8336109221f6eab5404a0c23b335bcd4a4a9 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 14:27:16 +0000 Subject: [PATCH 08/15] Implement Copilot feedback in tests --- tests/components/indevolt/test_binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/indevolt/test_binary_sensor.py b/tests/components/indevolt/test_binary_sensor.py index 28e9c3dab23eb..22d275f4a66cd 100644 --- a/tests/components/indevolt/test_binary_sensor.py +++ b/tests/components/indevolt/test_binary_sensor.py @@ -127,7 +127,7 @@ async def test_battery_pack_heating_filtering( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - # Get all sensor entities + # Get all binary sensor entities entity_entries = er.async_entries_for_config_entry( entity_registry, mock_config_entry.entry_id ) From d848cac7f8220d5c3e716b0b110339f4929227c0 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 16:49:39 +0200 Subject: [PATCH 09/15] Update tests/components/indevolt/test_binary_sensor.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/components/indevolt/test_binary_sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/indevolt/test_binary_sensor.py b/tests/components/indevolt/test_binary_sensor.py index 22d275f4a66cd..376975fa78458 100644 --- a/tests/components/indevolt/test_binary_sensor.py +++ b/tests/components/indevolt/test_binary_sensor.py @@ -123,9 +123,10 @@ async def test_battery_pack_heating_filtering( "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() + with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): + 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( From 7650f000002e6eab107e8d44cecb4ce5afce43cb Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 15:02:19 +0000 Subject: [PATCH 10/15] Various improvements to tests --- .../components/indevolt/test_binary_sensor.py | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/tests/components/indevolt/test_binary_sensor.py b/tests/components/indevolt/test_binary_sensor.py index 376975fa78458..1c6b3531d39a1 100644 --- a/tests/components/indevolt/test_binary_sensor.py +++ b/tests/components/indevolt/test_binary_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory +from indevolt_api import IndevoltBattery, IndevoltGrid import pytest from syrupy.assertion import SnapshotAssertion @@ -16,7 +17,6 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform -METER_CONNECTED_KEY = "7120" METER_CONNECTED_VALUE = 1000 METER_DISCONNECTED_VALUE = 1001 @@ -48,7 +48,9 @@ async def test_meter_connected_on_state( mock_config_entry: MockConfigEntry, ) -> None: """Test meter_connected reports ON when value is 1000 (Enable).""" - mock_indevolt.fetch_data.return_value[METER_CONNECTED_KEY] = METER_CONNECTED_VALUE + mock_indevolt.fetch_data.return_value[IndevoltGrid.METER_CONNECTED] = ( + METER_CONNECTED_VALUE + ) with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): await setup_integration(hass, mock_config_entry) @@ -66,7 +68,7 @@ async def test_meter_connected_off_state( mock_config_entry: MockConfigEntry, ) -> None: """Test meter_connected reports OFF when value is 1001 (Disable).""" - mock_indevolt.fetch_data.return_value[METER_CONNECTED_KEY] = ( + mock_indevolt.fetch_data.return_value[IndevoltGrid.METER_CONNECTED] = ( METER_DISCONNECTED_VALUE ) @@ -116,11 +118,11 @@ async def test_battery_pack_heating_filtering( # 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, + IndevoltBattery.PACK_1_SERIAL_NUMBER: "BAT001", + IndevoltBattery.PACK_2_SERIAL_NUMBER: "BAT002", + IndevoltBattery.PACK_3_SERIAL_NUMBER: None, + IndevoltBattery.PACK_4_SERIAL_NUMBER: "", + IndevoltBattery.PACK_5_SERIAL_NUMBER: None, } with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): @@ -134,16 +136,36 @@ async def test_battery_pack_heating_filtering( ) # Verify sensors for packs 1 and 2 exist (with SNs) - pack1_sensors = [e for e in entity_entries if "9096" in e.unique_id] - pack2_sensors = [e for e in entity_entries if "9112" in e.unique_id] + pack1_sensors = [ + e + for e in entity_entries + if e.unique_id.endswith(IndevoltBattery.PACK_1_HEATING_STATE) + ] + pack2_sensors = [ + e + for e in entity_entries + if e.unique_id.endswith(IndevoltBattery.PACK_2_HEATING_STATE) + ] 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 "9128" in e.unique_id] - pack4_sensors = [e for e in entity_entries if "9144" in e.unique_id] - pack5_sensors = [e for e in entity_entries if "9279" in e.unique_id] + pack3_sensors = [ + e + for e in entity_entries + if e.unique_id.endswith(IndevoltBattery.PACK_3_HEATING_STATE) + ] + pack4_sensors = [ + e + for e in entity_entries + if e.unique_id.endswith(IndevoltBattery.PACK_4_HEATING_STATE) + ] + pack5_sensors = [ + e + for e in entity_entries + if e.unique_id.endswith(IndevoltBattery.PACK_5_HEATING_STATE) + ] assert len(pack3_sensors) == 0 assert len(pack4_sensors) == 0 From 23cb5472a2b24591fffc3b30e51adf2e83b23c96 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 15:12:39 +0000 Subject: [PATCH 11/15] Improve test coverage via fixtures / snapshot updates --- tests/components/indevolt/fixtures/gen_1.json | 3 ++- tests/components/indevolt/fixtures/gen_2.json | 6 ++++++ .../indevolt/snapshots/test_binary_sensor.ambr | 16 ++++++++-------- .../indevolt/snapshots/test_diagnostics.ambr | 9 ++++++++- 4 files changed, 24 insertions(+), 10 deletions(-) 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 index 6a99c80861c6b..c122ef7f1da72 100644 --- a/tests/components/indevolt/snapshots/test_binary_sensor.ambr +++ b/tests/components/indevolt/snapshots/test_binary_sensor.ambr @@ -46,7 +46,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'on', }) # --- # name: test_binary_sensor[1][binary_sensor.bk1600_meter_connected-entry] @@ -97,7 +97,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'on', }) # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_1_electric_heating-entry] @@ -147,7 +147,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'on', }) # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_2_electric_heating-entry] @@ -197,7 +197,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'off', }) # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_3_electric_heating-entry] @@ -247,7 +247,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'on', }) # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_4_electric_heating-entry] @@ -297,7 +297,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'off', }) # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_battery_pack_5_electric_heating-entry] @@ -347,7 +347,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'on', }) # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_main_electric_heating-entry] @@ -397,7 +397,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'on', }) # --- # name: test_binary_sensor[2][binary_sensor.cms_sf2000_meter_connected-entry] 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', From 4bc1a1dfe90212885fb69b2e89832350a952bcb2 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 15:32:18 +0000 Subject: [PATCH 12/15] Incorporate copilot feedback --- homeassistant/components/indevolt/binary_sensor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py index 553c10cf02437..f921206a74df6 100644 --- a/homeassistant/components/indevolt/binary_sensor.py +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -26,6 +26,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): """Custom entity description class for Indevolt binary sensors.""" on_value: int = 1 + off_value: int = 0 generation: list[int] = field(default_factory=lambda: [1, 2]) @@ -35,6 +36,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): 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, @@ -144,7 +146,11 @@ def __init__( 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 is None: - return None - return raw_value == self.entity_description.on_value + if raw_value == self.entity_description.on_value: + return True + + if raw_value == self.entity_description.off_value: + return False + + return None From d0d002e1d16e629b660d77a8b09765b42e642f22 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 17:54:01 +0000 Subject: [PATCH 13/15] Process review feedback --- .../components/indevolt/test_binary_sensor.py | 121 +++++++----------- 1 file changed, 49 insertions(+), 72 deletions(-) diff --git a/tests/components/indevolt/test_binary_sensor.py b/tests/components/indevolt/test_binary_sensor.py index 1c6b3531d39a1..d88320a139c2d 100644 --- a/tests/components/indevolt/test_binary_sensor.py +++ b/tests/components/indevolt/test_binary_sensor.py @@ -4,12 +4,17 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from indevolt_api import IndevoltBattery, IndevoltGrid 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, Platform +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 @@ -17,15 +22,16 @@ 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" -ENTITY_ID_GEN1 = "binary_sensor.bk1600_meter_connected" @pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize("generation", [2, 1], indirect=True) +@pytest.mark.parametrize("generation", [1, 2], indirect=True) async def test_binary_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -41,43 +47,41 @@ async def test_binary_sensor( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize("generation", [1], indirect=True) -async def test_meter_connected_on_state( +@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 reports ON when value is 1000 (Enable).""" - mock_indevolt.fetch_data.return_value[IndevoltGrid.METER_CONNECTED] = ( - METER_CONNECTED_VALUE - ) + """Test meter_connected state transitions between ON, OFF, and unknown.""" - with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): - await setup_integration(hass, mock_config_entry) + # 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) - # Verify updated state (available) - assert (state := hass.states.get(ENTITY_ID_GEN1)) is not None - assert state.state == STATE_ON + 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() -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize("generation", [2], indirect=True) -async def test_meter_connected_off_state( - hass: HomeAssistant, - mock_indevolt: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test meter_connected reports OFF when value is 1001 (Disable).""" - mock_indevolt.fetch_data.return_value[IndevoltGrid.METER_CONNECTED] = ( - METER_DISCONNECTED_VALUE - ) + assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None + assert state.state == STATE_ON - with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): - await setup_integration(hass, mock_config_entry) + # 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() - # Verify updated state (off) assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -89,20 +93,14 @@ async def test_binary_sensor_availability( freezer: FrozenDateTimeFactory, ) -> None: """Test binary sensor availability when coordinator fails.""" - with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): - await setup_integration(hass, mock_config_entry) + await setup_integration(hass, mock_config_entry) - # Verify initial state (available) - assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None - assert state.state == STATE_OFF - - # Simulate a fetch error + # 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() - # Verify updated state (unavailable) assert (state := hass.states.get(ENTITY_ID_GEN2)) is not None assert state.state == STATE_UNAVAILABLE @@ -118,17 +116,16 @@ async def test_battery_pack_heating_filtering( # Mock battery pack data - only first two packs have SNs mock_indevolt.fetch_data.return_value = { - IndevoltBattery.PACK_1_SERIAL_NUMBER: "BAT001", - IndevoltBattery.PACK_2_SERIAL_NUMBER: "BAT002", - IndevoltBattery.PACK_3_SERIAL_NUMBER: None, - IndevoltBattery.PACK_4_SERIAL_NUMBER: "", - IndevoltBattery.PACK_5_SERIAL_NUMBER: None, + "9032": "BAT001", + "9051": "BAT002", + "9070": None, + "9165": "", + "9218": None, } - with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.BINARY_SENSOR]): - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + 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( @@ -136,36 +133,16 @@ async def test_battery_pack_heating_filtering( ) # Verify sensors for packs 1 and 2 exist (with SNs) - pack1_sensors = [ - e - for e in entity_entries - if e.unique_id.endswith(IndevoltBattery.PACK_1_HEATING_STATE) - ] - pack2_sensors = [ - e - for e in entity_entries - if e.unique_id.endswith(IndevoltBattery.PACK_2_HEATING_STATE) - ] + 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(IndevoltBattery.PACK_3_HEATING_STATE) - ] - pack4_sensors = [ - e - for e in entity_entries - if e.unique_id.endswith(IndevoltBattery.PACK_4_HEATING_STATE) - ] - pack5_sensors = [ - e - for e in entity_entries - if e.unique_id.endswith(IndevoltBattery.PACK_5_HEATING_STATE) - ] + 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 From 8cffb87427c0877c303875edabbf2b4d1b2c99a9 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 18:08:15 +0000 Subject: [PATCH 14/15] Use tuple instead of list for generation --- .../components/indevolt/binary_sensor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py index f921206a74df6..a001bc0f91de9 100644 --- a/homeassistant/components/indevolt/binary_sensor.py +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -1,6 +1,6 @@ """Binary sensor platform for Indevolt integration.""" -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Final from indevolt_api import IndevoltBattery, IndevoltGrid, IndevoltSystem @@ -27,7 +27,7 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): on_value: int = 1 off_value: int = 0 - generation: list[int] = field(default_factory=lambda: [1, 2]) + generation: tuple[int, ...] = (1, 2) BINARY_SENSORS: Final = ( @@ -44,49 +44,49 @@ class IndevoltBinarySensorEntityDescription(BinarySensorEntityDescription): # Electric Heating States IndevoltBinarySensorEntityDescription( key=IndevoltSystem.HEATING_STATE, - generation=[1], + generation=(1,), translation_key="electric_heating_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), IndevoltBinarySensorEntityDescription( key=IndevoltBattery.MAIN_HEATING_STATE, - generation=[2], + 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], + 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], + 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], + 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], + 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], + generation=(2,), translation_key="battery_pack_5_electric_heating_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, From 50b3a557186a4cd6bcb9ad546e13497a2c2a6520 Mon Sep 17 00:00:00 2001 From: "A. Gideonse" Date: Tue, 28 Apr 2026 22:17:54 +0000 Subject: [PATCH 15/15] Optimize excluded_keys logic --- homeassistant/components/indevolt/binary_sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/indevolt/binary_sensor.py b/homeassistant/components/indevolt/binary_sensor.py index a001bc0f91de9..109a9488f6bbd 100644 --- a/homeassistant/components/indevolt/binary_sensor.py +++ b/homeassistant/components/indevolt/binary_sensor.py @@ -113,11 +113,9 @@ async def async_setup_entry( device_gen = coordinator.generation excluded_keys: set[str] = set() - for pack_keys in BATTERY_PACK_SENSOR_KEYS: - sn_key = pack_keys[0] - + for sn_key, heating_key in BATTERY_PACK_SENSOR_KEYS: if not coordinator.data.get(sn_key): - excluded_keys.update(pack_keys) + excluded_keys.add(heating_key) async_add_entities( IndevoltBinarySensorEntity(coordinator, description)