diff --git a/.gitignore b/.gitignore index 41f0f9a..665af6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ */__pycache__/ *.pyc +.DS_Store diff --git a/README.md b/README.md index 58f2e37..56335b1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ After the installation, you can use this button to install the integration: - AC200L (untested) - AC200M - AC300 (tested) -- AC500 +- AC500 (tested) - EB3A - EP500 - EP500P diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 8d68e05..16fb215 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -38,9 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.data.get(CONF_ADDRESS) device_name = entry.data.get(CONF_NAME) use_controls = entry.data.get(CONF_USE_CONTROLS) - polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20) + polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 60) persistent_conn = entry.data.get(CONF_PERSISTENT_CONN, False) - polling_timeout = entry.data.get(CONF_POLLING_TIMEOUT, 45) + polling_timeout = entry.data.get(CONF_POLLING_TIMEOUT, 120) max_retries = entry.data.get(CONF_MAX_RETRIES, 5) if address is None: diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index 5bb013e..862bd0f 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -86,6 +86,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): e_name = f"{device_info.get('name')} {name}" self._address = address self._response_key = response_key + self._unavailable_counter = 5 self._attr_device_info = device_info self._attr_name = name diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index 8160342..a8c4532 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -14,6 +14,8 @@ class ProtocolV1Device(BluettiDevice): # Device info self.struct.add_string_field("device_type", 10, 6) self.struct.add_sn_field("serial_number", 17) + self.struct.add_version_field('arm_version', 23) + self.struct.add_version_field('dsp_version', 25) # Power IO self.struct.add_uint_field("dc_input_power", 36) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py index 6dd6fd0..92b6162 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -53,6 +53,8 @@ class DeviceReader: if filter_registers is not None: polling_commands = filter_registers pack_commands = [] + _LOGGER.info("Polling commands: " + ",".join([f"{c.starting_address}-{c.starting_address + c.quantity - 1}" for c in polling_commands])) + _LOGGER.info("Pack comands: " + ",".join([f"{c.starting_address}-{c.starting_address + c.quantity - 1}" for c in pack_commands])) parsed_data: dict = {} @@ -66,8 +68,8 @@ class DeviceReader: await self.client.connect() break except Exception as e: - if attempt == self.max_retries or attempt == 1: - raise # pass exception on max_retries attempt + if attempt == self.max_retries: + raise e # pass exception on max_retries attempt else: _LOGGER.warning( f"Connect unsucessful (attempt {attempt}): {e}. Retrying..." @@ -117,6 +119,11 @@ class DeviceReader: _LOGGER.warning("Pack polling failed (pack_num %i doesn't match expected %i)", set_pack, pack) continue + if self.bluetti_device.pack_num_max > 1: + # We need to wait after switching packs + # for the data to be available + await asyncio.sleep(5) + for command in pack_commands: # Request & parse result for each pack try: @@ -136,8 +143,8 @@ class DeviceReader: except ParseError: _LOGGER.warning("Got a parse exception...") - except TimeoutError: - _LOGGER.warning("Polling timed out. Trying again later") + except TimeoutError as err: + _LOGGER.error(f"Polling timed out ({self.polling_timeout}s). Trying again later", exc_info=err) return None except BleakError as err: _LOGGER.error("Bleak error: %s", err) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py index e1d5fa6..1c74219 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py @@ -28,6 +28,7 @@ class AC200L(ProtocolV1Device): self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + self.struct.add_version_field("pack_bms_version", 201) # Controls self.struct.add_bool_field("power_off", 3060) @@ -39,12 +40,9 @@ class AC200L(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(70, 5), - ReadHoldingRegisters(86, 3), - ReadHoldingRegisters(92, 1), - ReadHoldingRegisters(3060, 2), - ] + return self.struct.get_read_holding_registers( + filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands + ) @property def writable_ranges(self) -> List[range]: @@ -53,8 +51,6 @@ class AC200L(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), - ReadHoldingRegisters(105, 16) + ReadHoldingRegisters(91, 37), + ReadHoldingRegisters(201, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index fb0d97c..addc485 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -28,6 +28,7 @@ class AC200M(ProtocolV1Device): self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + self.struct.add_version_field("pack_bms_version", 201) # Controls self.struct.add_bool_field("power_off", 3060) @@ -39,12 +40,9 @@ class AC200M(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(70, 5), - ReadHoldingRegisters(86, 3), - ReadHoldingRegisters(92, 1), - ReadHoldingRegisters(3060, 2), - ] + return self.struct.get_read_holding_registers( + filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands + ) @property def writable_ranges(self) -> List[range]: @@ -53,8 +51,6 @@ class AC200M(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), - ReadHoldingRegisters(105, 16) + ReadHoldingRegisters(91, 37), + ReadHoldingRegisters(201, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index e7a9303..7311ac6 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -54,17 +54,9 @@ class AC300(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(70, 11), - ReadHoldingRegisters(86, 3), - ReadHoldingRegisters(92, 1), - ReadHoldingRegisters(3001, 1), - ReadHoldingRegisters(3004, 2), - ReadHoldingRegisters(3011, 1), - ReadHoldingRegisters(3013, 1), - ReadHoldingRegisters(3015, 2), - ReadHoldingRegisters(3061, 1), - ] + return self.struct.get_read_holding_registers( + filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands + ) @property def writable_ranges(self) -> List[range]: @@ -76,9 +68,6 @@ class AC300(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), - ReadHoldingRegisters(105, 16), + ReadHoldingRegisters(91, 37), ReadHoldingRegisters(201, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 3159dde..b921e44 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -34,6 +34,7 @@ class AC500(ProtocolV1Device): self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + self.struct.add_version_field('pack_bms_version', 201) # Controls self.struct.add_enum_field("ups_mode", 3001, UpsMode) @@ -49,21 +50,13 @@ class AC500(ProtocolV1Device): @property def pack_num_max(self): - return 4 + return 6 @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(70, 11), - ReadHoldingRegisters(86, 3), - ReadHoldingRegisters(92, 1), - ReadHoldingRegisters(3001, 1), - ReadHoldingRegisters(3004, 2), - ReadHoldingRegisters(3011, 1), - ReadHoldingRegisters(3013, 1), - ReadHoldingRegisters(3015, 2), - ReadHoldingRegisters(3061, 1), - ] + return self.struct.get_read_holding_registers( + filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands + ) @property def writable_ranges(self) -> List[range]: @@ -75,8 +68,6 @@ class AC500(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), - ReadHoldingRegisters(105, 16), + ReadHoldingRegisters(91, 37), + ReadHoldingRegisters(201, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py index 644ac26..7b9eb56 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py @@ -25,13 +25,7 @@ class EB3A(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(77, 1), - ReadHoldingRegisters(86, 1), - ReadHoldingRegisters(3034, 1), - ReadHoldingRegisters(3060, 1), - ReadHoldingRegisters(3063, 5), - ] + return self.struct.get_read_holding_registers() @property def writable_ranges(self) -> List[range]: diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index 1554ce9..78a7a6c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -34,6 +34,7 @@ class EP500(ProtocolV1Device): self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + self.struct.add_version_field("pack_bms_version", 201) # Controls self.struct.add_enum_field("ups_mode", 3001, UpsMode) @@ -49,17 +50,9 @@ class EP500(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(70, 11), - ReadHoldingRegisters(86, 3), - ReadHoldingRegisters(92, 1), - ReadHoldingRegisters(3001, 1), - ReadHoldingRegisters(3004, 2), - ReadHoldingRegisters(3011, 1), - ReadHoldingRegisters(3013, 1), - ReadHoldingRegisters(3015, 2), - ReadHoldingRegisters(3061, 1), - ] + return self.struct.get_read_holding_registers( + filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands + ) @property def writable_ranges(self) -> List[range]: @@ -71,8 +64,6 @@ class EP500(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), - ReadHoldingRegisters(105, 16), + ReadHoldingRegisters(91, 37), + ReadHoldingRegisters(201, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index ba19241..740e580 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -34,6 +34,7 @@ class EP500P(ProtocolV1Device): self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + self.struct.add_version_field("pack_bms_version", 201) # Controls self.struct.add_enum_field("ups_mode", 3001, UpsMode) @@ -49,17 +50,9 @@ class EP500P(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(70, 11), - ReadHoldingRegisters(86, 3), - ReadHoldingRegisters(92, 1), - ReadHoldingRegisters(3001, 1), - ReadHoldingRegisters(3004, 2), - ReadHoldingRegisters(3011, 1), - ReadHoldingRegisters(3013, 1), - ReadHoldingRegisters(3015, 2), - ReadHoldingRegisters(3061, 1), - ] + return self.struct.get_read_holding_registers( + filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands + ) @property def writable_ranges(self) -> List[range]: @@ -71,8 +64,6 @@ class EP500P(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), - ReadHoldingRegisters(105, 16), + ReadHoldingRegisters(91, 37), + ReadHoldingRegisters(201, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py index 282d1f4..8404a0e 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py @@ -97,28 +97,4 @@ class EP600(ProtocolV2Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: - return super().polling_commands + [ - ReadHoldingRegisters(2022, 2), - ReadHoldingRegisters(2213, 5), - ReadHoldingRegisters(1212, 3), - ReadHoldingRegisters(1220, 3), - ReadHoldingRegisters(1228, 3), - ReadHoldingRegisters(1236, 3), - ReadHoldingRegisters(1244, 3), - ReadHoldingRegisters(1300, 1), - ReadHoldingRegisters(1313, 3), - ReadHoldingRegisters(1319, 3), - ReadHoldingRegisters(1325, 3), - ReadHoldingRegisters(1500, 1), - ReadHoldingRegisters(1510, 3), - ReadHoldingRegisters(1517, 3), - ReadHoldingRegisters(1524, 3), - ReadHoldingRegisters(152, 1), - ReadHoldingRegisters(156, 1), - ReadHoldingRegisters(158, 1), - # KM - Consumption - ReadHoldingRegisters(1430, 15), - # KM - Totals - ReadHoldingRegisters(142, 5), - - ] + return self.struct.get_read_holding_registers() diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 8b663df..6bc89f8 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -125,6 +125,14 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { "ac_input_power": PowerFieldAttributes("AC Input Power"), "ac_output_power": PowerFieldAttributes("AC Output Power"), "dc_output_power": PowerFieldAttributes("DC Output Power"), + "dsp_version": FieldAttributes( + type=FieldType.NUMERIC, + name=f"DSP Version", + ), + "arm_version": FieldAttributes( + type=FieldType.NUMERIC, + name=f"ARM Version", + ), "power_generation": EnergyFieldAttributes("Total Power Generation"), "total_battery_percent": FieldAttributes( type=FieldType.NUMERIC, diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index 278d5ab..3990739 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -4,6 +4,7 @@ from decimal import Decimal from enum import Enum +from .commands import ReadHoldingRegisters import struct from typing import Any, List, Optional, Tuple, Type @@ -179,6 +180,35 @@ class DeviceStruct: def add_sn_field(self, name: str, address: int): self.fields.append(SerialNumberField(name, address)) + + def get_read_holding_registers(self, tolerance=20, filter=lambda address: True): + address_set = set() + for field in self.fields: + for address in range(field.address, field.address + field.size): + address_set.add(address) + # Filter out addresses that we specificly + # specified to exclude. + for address in set(address_set): + if not filter(address): + address_set.remove(address) + # Fast path: no addresses -> no commands. + if not address_set: + return [] + + commands = [] + + address_list = sorted(address_set) + start = address_list[0] + last = address_list[0] + for address in address_list[1:]: + # Create a new group if there's a gap + # in the addresses. + if address >= last + tolerance: + commands.append(ReadHoldingRegisters(start, last - start + 1)) + start = address + last = address + commands.append(ReadHoldingRegisters(start, last - start + 1)) + return commands def parse(self, starting_address: int, data: bytes) -> dict: # Offsets and size are counted in 2 byte chunks, so for the range we diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 0f16d01..aedc0d8 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -42,6 +42,8 @@ DIAGNOSTIC_FIELDS = [ "max_ac_output_power", "max_ac_output_current", "bcu_version", + "dsp_version", + "arm_version", "safety_module_version", "high_voltage_module_version", "power_generation",