From cfa74dacf69863431ed7e21be0ccd396fcc4ac4c Mon Sep 17 00:00:00 2001 From: Harvey Lelliott <42912136+flip-dots@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:11:50 +0100 Subject: [PATCH 1/7] Add support for Solarbank 2 (Some things still missing) --- custom_components/solix_ble/__init__.py | 3 + custom_components/solix_ble/const.py | 3 + custom_components/solix_ble/sensor.py | 267 +++++++++++++++++++++++- tests/__init__.py | 43 ++++ tests/test_config_flow.py | 3 + tests/test_init.py | 4 + tests/test_sensor.py | 9 + 7 files changed, 323 insertions(+), 9 deletions(-) diff --git a/custom_components/solix_ble/__init__.py b/custom_components/solix_ble/__init__.py index 94f69fa..1cf9fbd 100644 --- a/custom_components/solix_ble/__init__.py +++ b/custom_components/solix_ble/__init__.py @@ -21,6 +21,7 @@ Generic, PrimeCharger160w, PrimeCharger250w, + Solarbank2, SolixBLEDevice, ) @@ -52,6 +53,8 @@ def get_power_station_class(model: Models) -> SolixBLEDevice: return PrimeCharger160w elif model is Models.PRIME_CHARGER_250: return PrimeCharger250w + elif model is Models.SOLARBANK_2: + return Solarbank2 elif model is Models.UNKNOWN: return Generic else: diff --git a/custom_components/solix_ble/const.py b/custom_components/solix_ble/const.py index 3f3da50..6b663d4 100644 --- a/custom_components/solix_ble/const.py +++ b/custom_components/solix_ble/const.py @@ -20,6 +20,8 @@ OVERLOAD_STATUS_C300DC_STRINGS = ["Unknown", "None", "USB C1", "USB C2", "USB C3"] +GRID_STATUS_STRINGS = ["Unknown", "Ok", "Ok?", "Connecting", "No grid"] + class Models(Enum): C300 = "C300(X)" @@ -31,4 +33,5 @@ class Models(Enum): F3800 = "F3800" PRIME_CHARGER_160 = "Prime Charger (160w)" PRIME_CHARGER_250 = "Prime Charger (250w)" + SOLARBANK_2 = "Solarbank 2" UNKNOWN = "Unknown" diff --git a/custom_components/solix_ble/sensor.py b/custom_components/solix_ble/sensor.py index c46e92c..8d72b0d 100644 --- a/custom_components/solix_ble/sensor.py +++ b/custom_components/solix_ble/sensor.py @@ -22,12 +22,14 @@ F3800, PrimeCharger160w, PrimeCharger250w, + Solarbank2, SolixBLEDevice, ) from .const import ( CHARGING_STATUS_C300_STRINGS, CHARGING_STATUS_F3800_STRINGS, + GRID_STATUS_STRINGS, LIGHT_STATUS_STRINGS, OVERLOAD_STATUS_C300DC_STRINGS, PORT_STATUS_STRINGS, @@ -101,7 +103,7 @@ async def async_setup_entry( ), # Battery percentage sensor - if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800]: + if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -112,6 +114,30 @@ async def async_setup_entry( ) ) + # Battery charge power sensor + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Battery charge power", + "W", + "battery_charge_power", + SensorDeviceClass.POWER, + ) + ) + + # Battery discharge power sensor + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Battery discharge power", + "W", + "battery_discharge_power", + SensorDeviceClass.POWER, + ) + ) + # Battery health sensor if type(device) in [C300DC, C800, C1000, C1000G2, F2000]: sensors.append( @@ -124,8 +150,32 @@ async def async_setup_entry( ) ) + # Battery charged energy (energy in) + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Battery input energy", + "Wh", + "charged_energy", + SensorDeviceClass.ENERGY, + ) + ) + + # Battery discharged energy (energy out) + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Battery output energy", + "Wh", + "output_energy", + SensorDeviceClass.ENERGY, + ) + ) + # Temperature sensor - if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800]: + if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -145,7 +195,7 @@ async def async_setup_entry( ) # Total power out sensor - if type(device) in [C300, C300DC, C800, C1000, C1000G2, F3800]: + if type(device) in [C300, C300DC, C800, C1000, C1000G2, F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, "Total Power Out", "W", "power_out", SensorDeviceClass.POWER @@ -165,7 +215,7 @@ async def async_setup_entry( ), # AC power out sensor - if type(device) in [C300, C800, C1000, C1000G2, F2000, F3800]: + if type(device) in [C300, C800, C1000, C1000G2, F2000, F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -204,7 +254,7 @@ async def async_setup_entry( ) # Solar power in - if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800]: + if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -215,6 +265,19 @@ async def async_setup_entry( ) ) + # Solar yield + # TODO: Unit may not be correct + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "PV Yield", + "Wh", + "pv_yield", + SensorDeviceClass.ENERGY, + ) + ) + # DC power out if type(device) in [C300, C300DC, C1000G2]: sensors.append( @@ -669,8 +732,20 @@ async def async_setup_entry( ) ) + # Error status + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Error code", + None, + "error_code", + state_class=None, + ) + ) + # Firmware version - if type(device) in [C300, C300DC, C800, C1000, F2000, F3800]: + if type(device) in [C300, C300DC, C800, C1000, F2000, F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -682,7 +757,7 @@ async def async_setup_entry( ) # Serial number - if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800]: + if type(device) in [C300, C300DC, C800, C1000, C1000G2, F2000, F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -718,7 +793,7 @@ async def async_setup_entry( ) # Average battery percentage across all batteries - if type(device) in [F3800]: + if type(device) in [F3800, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -742,7 +817,7 @@ async def async_setup_entry( ) # Expansion battery firmware version - if type(device) in [C1000, F2000]: + if type(device) in [C1000, F2000, Solarbank2]: sensors.append( SolixSensorEntity( device, @@ -764,6 +839,180 @@ async def async_setup_entry( ) ) + ###################### + # Solar bank sensors # + ###################### + + # Grid to home power + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Grid to Home power", + "W", + "grid_to_home_power", + SensorDeviceClass.POWER, + ) + ) + + # PV to grid power + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "PV to Grid power", + "W", + "pv_to_grid_power", + SensorDeviceClass.POWER, + ) + ) + + # Grid import energy + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Grid import energy", + "Wh", + "grid_import_energy", + SensorDeviceClass.ENERGY, + ) + ) + + # Grid export energy + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Grid export energy", + "Wh", + "grid_export_energy", + SensorDeviceClass.ENERGY, + ) + ) + + # House demand (power used by house) + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "House demand power", + "W", + "house_demand", + SensorDeviceClass.POWER, + ) + ) + + # House demand (power used by house) + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "House consumed energy", + "Wh", + "consumed_energy", + SensorDeviceClass.ENERGY, + ) + ) + + # Power out of the built-in sockets + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Sockets AC Power Out", + "W", + "ac_power_out_sockets", + SensorDeviceClass.POWER, + ) + ) + + # Max load + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Maximum load", + "W", + "max_load", + SensorDeviceClass.POWER, + ) + ) + + # Solar PV power in for port 1 + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Solar Power In Port 1", + "W", + "solar_pv_1_power_in", + SensorDeviceClass.POWER, + ) + ) + + # Solar PV power in for port 2 + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Solar Power In Port 2", + "W", + "solar_pv_2_power_in", + SensorDeviceClass.POWER, + ) + ) + + # Solar PV power in for port 3 + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Solar Power In Port 3", + "W", + "solar_pv_3_power_in", + SensorDeviceClass.POWER, + ) + ) + + # Solar PV power in for port 4 + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Solar Power In Port 4", + "W", + "solar_pv_4_power_in", + SensorDeviceClass.POWER, + ) + ) + + # Grid status + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Grid Status", + None, + "grid_status", + SensorDeviceClass.ENUM, + GRID_STATUS_STRINGS, + None, + ) + ) + + # Heater status + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Heater on", + None, + "battery_heating", + None, + ) + ), + async_add_entities(sensors) diff --git a/tests/__init__.py b/tests/__init__.py index 3b82e21..5f6ba90 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -168,6 +168,13 @@ def get_service_info(self) -> BluetoothServiceInfoBleak: model_class=Models.PRIME_CHARGER_250, ) +MOCK_SOLAR_BANK_2_DETAILS = MockDeviceDetails( + name="Solar Bank 2", + addr="AA:BB:CC:DD:00:02", + model_string="Solarbank 2", + model_class=Models.SOLARBANK_2, +) + MOCK_UNKNOWN_DETAILS = MockDeviceDetails( name="Anker SOLIX IDK", addr="AA:BB:CC:DD:EE:04", @@ -337,4 +344,40 @@ def get_service_info(self) -> BluetoothServiceInfoBleak: "usb_port_a2": ("status_usb_a2", PortStatus.NOT_CONNECTED), } + +# Sometimes the method name we are patching and the +# entity ID do not line up, so a tuple is used to +# manually specify it +MOCK_SOLAR_BANK_2_TEST_DATA = { + "serial_number": "abcdef", + "battery_percentage": 56, + "software_version": "0.0.1", + "software_version_expansion": "0.0.2", + "temperature": -5, + "solar_power_in": 150.3, + "ac_power_out": 700.1, + "battery_percentage_aggregate": 90, + "battery_charge_power": 0, + "pv_yield": 1053.2, + "charged_energy": 124.3, + "output_energy": 12.7, + "battery_discharge_power": 12.4, + "grid_to_home_power": 12.5, + "pv_to_grid_power": 1245.1, + "grid_import_energy": 12.3, + "grid_export_energy": 1257.3, + "house_demand": 124.6, + "ac_power_out_sockets": 1.67, + "max_load": 13.75, + "consumed_energy": 124.5, + "solar_pv_1_power_in": 13.1, + "solar_pv_2_power_in": 13.2, + "solar_pv_3_power_in": 13.3, + "solar_pv_4_power_in": 13.4, + "power_out": 14.745, + "error_code": 0, + "grid_status": -1, + "battery_heating": True, +} + MOCK_UNKNOWN_TEST_DATA = {} diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 895ceaf..562f50a 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -24,6 +24,7 @@ MOCK_F3800_DETAILS, MOCK_PRIME_160_DETAILS, MOCK_PRIME_250_DETAILS, + MOCK_SOLAR_BANK_2_DETAILS, MOCK_UNKNOWN_DETAILS, BLEDevice, MockDeviceDetails, @@ -45,6 +46,7 @@ MOCK_F3800_DETAILS, MOCK_PRIME_160_DETAILS, MOCK_PRIME_250_DETAILS, + MOCK_SOLAR_BANK_2_DETAILS, MOCK_UNKNOWN_DETAILS, ], ids=[ @@ -59,6 +61,7 @@ "f3800", "prime_160w", "prime_250w", + "solar_bank_2", "unknown", ], ) diff --git a/tests/test_init.py b/tests/test_init.py index 98f5c02..2560c78 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -26,6 +26,7 @@ MOCK_F3800_DETAILS, MOCK_PRIME_160_DETAILS, MOCK_PRIME_250_DETAILS, + MOCK_SOLAR_BANK_2_DETAILS, MOCK_UNKNOWN_DETAILS, MockDeviceDetails, ) @@ -46,6 +47,9 @@ pytest.param(MOCK_F3800_DETAILS, MOCK_F3800_DETAILS, id="f3800"), pytest.param(MOCK_PRIME_160_DETAILS, MOCK_PRIME_160_DETAILS, id="prime_160w"), pytest.param(MOCK_PRIME_250_DETAILS, MOCK_PRIME_250_DETAILS, id="prime_250w"), + pytest.param( + MOCK_SOLAR_BANK_2_DETAILS, MOCK_SOLAR_BANK_2_DETAILS, id="solar_bank_2" + ), pytest.param(MOCK_UNKNOWN_DETAILS, MOCK_UNKNOWN_DETAILS, id="unknown"), ], indirect=["mock_config_entry"], diff --git a/tests/test_sensor.py b/tests/test_sensor.py index b184163..b22b8fd 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -30,6 +30,8 @@ MOCK_PRIME_160_TEST_DATA, MOCK_PRIME_250_DETAILS, MOCK_PRIME_250_TEST_DATA, + MOCK_SOLAR_BANK_2_DETAILS, + MOCK_SOLAR_BANK_2_TEST_DATA, MOCK_UNKNOWN_DETAILS, MOCK_UNKNOWN_TEST_DATA, MockDeviceDetails, @@ -94,6 +96,13 @@ MOCK_PRIME_250_TEST_DATA, id="prime_250w", ), + pytest.param( + MOCK_SOLAR_BANK_2_DETAILS, + MOCK_SOLAR_BANK_2_DETAILS, + "Solarbank2", + MOCK_SOLAR_BANK_2_TEST_DATA, + id="solar_bank_2", + ), pytest.param( MOCK_UNKNOWN_DETAILS, MOCK_UNKNOWN_DETAILS, From cafbceef72fcc4c7494d0c9a8c24abbe246de846 Mon Sep 17 00:00:00 2001 From: Harvey Lelliott <42912136+flip-dots@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:15:45 +0100 Subject: [PATCH 2/7] Use latest stable SolixBLE --- custom_components/solix_ble/manifest.json | 2 +- requirements_dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/solix_ble/manifest.json b/custom_components/solix_ble/manifest.json index d64dc3c..59bb557 100644 --- a/custom_components/solix_ble/manifest.json +++ b/custom_components/solix_ble/manifest.json @@ -14,7 +14,7 @@ "integration_type": "device", "iot_class": "local_push", "issue_tracker": "https://github.com/flip-dots/HaSolixBLE/issues", - "requirements": ["SolixBLE==3.6.0"], + "requirements": ["SolixBLE==3.7.0"], "loggers": ["bleak", "SolixBLE"], "version": "3.4.0" } \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index d240e34..8509e1d 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,4 +4,4 @@ pytest-homeassistant-custom-component pyserial pytest-cov aiousbwatcher -SolixBLE==3.6.0 +SolixBLE==3.7.0 From 2dcf6c78fa424f4f2a8c5520941e1a2f3fd9ccc8 Mon Sep 17 00:00:00 2001 From: Harvey Lelliott <42912136+flip-dots@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:23:52 +0100 Subject: [PATCH 3/7] Fix sensors for solarbank 2 --- custom_components/solix_ble/const.py | 62 +++++++++++++++++ custom_components/solix_ble/sensor.py | 98 +++++++++++++++++++++++---- 2 files changed, 145 insertions(+), 15 deletions(-) diff --git a/custom_components/solix_ble/const.py b/custom_components/solix_ble/const.py index 6b663d4..93fb380 100644 --- a/custom_components/solix_ble/const.py +++ b/custom_components/solix_ble/const.py @@ -4,8 +4,18 @@ DOMAIN = "solix_ble" + +############### +# Port status # +############### + PORT_STATUS_STRINGS = ["Unknown", "Not connected", "Output", "Input"] + +################### +# Charging status # +################### + CHARGING_STATUS_C300_STRINGS = ["Unknown", "Idle", "Discharging", "Charging"] CHARGING_STATUS_F3800_STRINGS = [ @@ -16,13 +26,65 @@ "Charging (Both)", ] + +################ +# Light status # +################ + LIGHT_STATUS_STRINGS = ["Unknown", "Off", "Low", "Medium", "High"] +LIGHT_STATUS_SB2_STRINGS = ["Unknown", "Normal", "Mood"] + + +################### +# Overload status # +################### + OVERLOAD_STATUS_C300DC_STRINGS = ["Unknown", "None", "USB C1", "USB C2", "USB C3"] + +################### +# Max load status # +################### + +MAX_LOAD_SB2_STRINGS = ["Unknown", "350w", "600w", "800w", "1000w"] + + +############### +# Grid status # +############### + GRID_STATUS_STRINGS = ["Unknown", "Ok", "Ok?", "Connecting", "No grid"] +############## +# Usage mode # +############## + +USAGE_MODE_SB2_STRINGS = [ + "Unknown", + "Manual", + "Smart meter", + "Smart plugs", + "Backup", + "Use Time", + "Smart", + "Time slot", +] + + +################## +# Battery cutoff # +################## + +CUT_OFF_SB2_STRINGS = ["Unknown", "5%", "10%"] + + +#################### +# Supported models # +#################### + + class Models(Enum): C300 = "C300(X)" C300DC = "C300(X) DC" diff --git a/custom_components/solix_ble/sensor.py b/custom_components/solix_ble/sensor.py index 8d72b0d..768b48c 100644 --- a/custom_components/solix_ble/sensor.py +++ b/custom_components/solix_ble/sensor.py @@ -29,10 +29,14 @@ from .const import ( CHARGING_STATUS_C300_STRINGS, CHARGING_STATUS_F3800_STRINGS, + CUT_OFF_SB2_STRINGS, GRID_STATUS_STRINGS, + LIGHT_STATUS_SB2_STRINGS, LIGHT_STATUS_STRINGS, + MAX_LOAD_SB2_STRINGS, OVERLOAD_STATUS_C300DC_STRINGS, PORT_STATUS_STRINGS, + USAGE_MODE_SB2_STRINGS, ) _LOGGER = logging.getLogger(__name__) @@ -156,9 +160,10 @@ async def async_setup_entry( SolixSensorEntity( device, "Battery input energy", - "Wh", + "kWh", "charged_energy", SensorDeviceClass.ENERGY, + state_class=None, ) ) @@ -168,9 +173,38 @@ async def async_setup_entry( SolixSensorEntity( device, "Battery output energy", - "Wh", + "kWh", "output_energy", SensorDeviceClass.ENERGY, + state_class=None, + ) + ) + + # Output cutoff thresholds + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Output cutoff threshold", + None, + "output_cutoff_data", + SensorDeviceClass.ENUM, + CUT_OFF_SB2_STRINGS, + state_class=None, + ) + ) + + # Input cutoff thresholds + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Input cutoff threshold", + None, + "input_cutoff_data", + SensorDeviceClass.ENUM, + CUT_OFF_SB2_STRINGS, + state_class=None, ) ) @@ -212,7 +246,7 @@ async def async_setup_entry( "ac_power_in", SensorDeviceClass.POWER, ) - ), + ) # AC power out sensor if type(device) in [C300, C800, C1000, C1000G2, F2000, F3800, Solarbank2]: @@ -224,7 +258,7 @@ async def async_setup_entry( "ac_power_out", SensorDeviceClass.POWER, ) - ), + ) # AC output on/off sensor if type(device) in [C300, C800, C1000, C1000G2, F3800]: @@ -266,15 +300,15 @@ async def async_setup_entry( ) # Solar yield - # TODO: Unit may not be correct if type(device) in [Solarbank2]: sensors.append( SolixSensorEntity( device, "PV Yield", - "Wh", + "kWh", "pv_yield", SensorDeviceClass.ENERGY, + state_class=None, ) ) @@ -873,9 +907,10 @@ async def async_setup_entry( SolixSensorEntity( device, "Grid import energy", - "Wh", + "kWh", "grid_import_energy", SensorDeviceClass.ENERGY, + state_class=None, ) ) @@ -885,9 +920,10 @@ async def async_setup_entry( SolixSensorEntity( device, "Grid export energy", - "Wh", + "kWh", "grid_export_energy", SensorDeviceClass.ENERGY, + state_class=None, ) ) @@ -909,9 +945,10 @@ async def async_setup_entry( SolixSensorEntity( device, "House consumed energy", - "Wh", + "kWh", "consumed_energy", SensorDeviceClass.ENERGY, + state_class=None, ) ) @@ -920,7 +957,7 @@ async def async_setup_entry( sensors.append( SolixSensorEntity( device, - "Sockets AC Power Out", + "AC Power Out Sockets", "W", "ac_power_out_sockets", SensorDeviceClass.POWER, @@ -933,9 +970,24 @@ async def async_setup_entry( SolixSensorEntity( device, "Maximum load", - "W", + None, "max_load", - SensorDeviceClass.POWER, + SensorDeviceClass.ENUM, + MAX_LOAD_SB2_STRINGS, + state_class=None, + ) + ) + + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Usage mode", + None, + "usage_mode", + SensorDeviceClass.ENUM, + USAGE_MODE_SB2_STRINGS, + state_class=None, ) ) @@ -987,6 +1039,20 @@ async def async_setup_entry( ) ) + # Solarbank light status + if type(device) in [Solarbank2]: + sensors.append( + SolixSensorEntity( + device, + "Status light", + None, + "light_mode", + SensorDeviceClass.ENUM, + LIGHT_STATUS_SB2_STRINGS, + None, + ) + ) + # Grid status if type(device) in [Solarbank2]: sensors.append( @@ -1006,12 +1072,12 @@ async def async_setup_entry( sensors.append( SolixSensorEntity( device, - "Heater on", + "Battery Heating", None, "battery_heating", None, ) - ), + ) async_add_entities(sensors) @@ -1074,7 +1140,9 @@ def _update_updatable_attributes(self) -> None: # If enum use enum strings elif self._attr_device_class == SensorDeviceClass.ENUM: - self._attr_native_value = self._attr_options[attribute_value.value + 1] + self._attr_native_value = self._attr_options[ + list(type(attribute_value)).index(attribute_value) + ] # Else pass through value else: From 43c1635218f86c02c94ec6d8e057fd4bb7ebd8e8 Mon Sep 17 00:00:00 2001 From: Harvey Lelliott <42912136+flip-dots@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:24:00 +0100 Subject: [PATCH 4/7] Fix tests for solarbank 2 --- tests/__init__.py | 41 ++++++++++++++++++++++++++--------------- tests/test_sensor.py | 5 +++++ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 5f6ba90..0c76580 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,13 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from habluetooth import BluetoothServiceInfoBleak from SolixBLE import LightStatus, PortStatus +from SolixBLE.devices.solarbank2 import ( + GridStatus, + LightMode, + MaxLoadSB2, + SBPowerCutoff, + SBUsageMode, +) from custom_components.solix_ble.const import Models @@ -349,34 +356,38 @@ def get_service_info(self) -> BluetoothServiceInfoBleak: # entity ID do not line up, so a tuple is used to # manually specify it MOCK_SOLAR_BANK_2_TEST_DATA = { - "serial_number": "abcdef", "battery_percentage": 56, - "software_version": "0.0.1", - "software_version_expansion": "0.0.2", + "software_version": ("firmware_version", "0.0.1"), + "software_version_expansion": ("expansion_battery_firmware_version", "0.0.2"), + "serial_number": "0.1.2.3", "temperature": -5, "solar_power_in": 150.3, "ac_power_out": 700.1, - "battery_percentage_aggregate": 90, + "battery_percentage_aggregate": ("average_battery_percentage", 90), "battery_charge_power": 0, "pv_yield": 1053.2, - "charged_energy": 124.3, - "output_energy": 12.7, + "charged_energy": ("battery_input_energy", 124.3), + "output_energy": ("battery_output_energy", 12.7), + "output_cutoff_data": ("output_cutoff_threshold", SBPowerCutoff.P5, "5%"), + "input_cutoff_data": ("input_cutoff_threshold", SBPowerCutoff.P10, "10%"), "battery_discharge_power": 12.4, "grid_to_home_power": 12.5, "pv_to_grid_power": 1245.1, "grid_import_energy": 12.3, "grid_export_energy": 1257.3, - "house_demand": 124.6, + "house_demand": ("house_demand_power", 124.6), "ac_power_out_sockets": 1.67, - "max_load": 13.75, - "consumed_energy": 124.5, - "solar_pv_1_power_in": 13.1, - "solar_pv_2_power_in": 13.2, - "solar_pv_3_power_in": 13.3, - "solar_pv_4_power_in": 13.4, - "power_out": 14.745, + "max_load": ("maximum_load", MaxLoadSB2.W350, "350w"), + "usage_mode": ("usage_mode", SBUsageMode.MANUAL, "Manual"), + "consumed_energy": ("house_consumed_energy", 124.5), + "solar_pv_1_power_in": ("solar_power_in_port_1", 13.1), + "solar_pv_2_power_in": ("solar_power_in_port_2", 13.2), + "solar_pv_3_power_in": ("solar_power_in_port_3", 13.3), + "solar_pv_4_power_in": ("solar_power_in_port_4", 13.4), + "power_out": ("total_power_out", 14.745), "error_code": 0, - "grid_status": -1, + "light_mode": ("status_light", LightMode.NORMAL, "Normal"), + "grid_status": ("grid_status", GridStatus.OK, "Ok"), "battery_heating": True, } diff --git a/tests/test_sensor.py b/tests/test_sensor.py index b22b8fd..4f51118 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -171,8 +171,10 @@ async def test_sensor_entities( # If the entity ID is manually specified use that rather # than using the method name of the underlying class + str_value = None if type(value) is tuple: key = value[0] + str_value = value[2] if len(value) == 3 else None value = value[1] # There are not enough words in the universe to express how @@ -185,6 +187,9 @@ async def test_sensor_entities( elif type(value) is PortStatus or type(value) is LightStatus: value = value.name.capitalize().replace("_", " ") + elif str_value is not None: + value = str_value + else: value = f"{value}" From baf8f1c41d51a923c0074aaae5c8c6dd60cb67e1 Mon Sep 17 00:00:00 2001 From: jul1an-s Date: Mon, 20 Apr 2026 16:03:55 +0200 Subject: [PATCH 5/7] Renamed output_energy Sensor to account for energy bypassing the battery --- custom_components/solix_ble/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/solix_ble/sensor.py b/custom_components/solix_ble/sensor.py index 768b48c..9d8bdd3 100644 --- a/custom_components/solix_ble/sensor.py +++ b/custom_components/solix_ble/sensor.py @@ -167,12 +167,12 @@ async def async_setup_entry( ) ) - # Battery discharged energy (energy out) + # Solarbank dispensed energy (energy out) if type(device) in [Solarbank2]: sensors.append( SolixSensorEntity( device, - "Battery output energy", + "Total output energy", "kWh", "output_energy", SensorDeviceClass.ENERGY, From ed9740122e93a44f2aae917f932eaf4b30156af8 Mon Sep 17 00:00:00 2001 From: Harvey <42912136+flip-dots@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:14:39 +0100 Subject: [PATCH 6/7] Use updated name for battery output energy --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 0c76580..4a10510 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -367,7 +367,7 @@ def get_service_info(self) -> BluetoothServiceInfoBleak: "battery_charge_power": 0, "pv_yield": 1053.2, "charged_energy": ("battery_input_energy", 124.3), - "output_energy": ("battery_output_energy", 12.7), + "output_energy": ("total_output_energy", 12.7), "output_cutoff_data": ("output_cutoff_threshold", SBPowerCutoff.P5, "5%"), "input_cutoff_data": ("input_cutoff_threshold", SBPowerCutoff.P10, "10%"), "battery_discharge_power": 12.4, From 98755a5b871f9513fdd6cac634eeb60a7c11350e Mon Sep 17 00:00:00 2001 From: Harvey <42912136+flip-dots@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:14:56 +0100 Subject: [PATCH 7/7] Add missing dependencies for CI/CD --- requirements_dev.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index 63795d7..b14174d 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,4 +4,6 @@ pytest-homeassistant-custom-component pyserial pytest-cov aiousbwatcher +ruff +mypy SolixBLE==3.8.0