Merge pull request #118 from AChep/main

Various imprvements for AC300 & AC500
This commit is contained in:
Patrick762 authored and GitHub committed 2024-08-04 12:07:43 +02:00
commit 52fd1932fa
17 files changed
+95 -120

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
*/__pycache__/ */__pycache__/
*.pyc *.pyc
.DS_Store
+1 -1
View File
@@ -24,7 +24,7 @@ After the installation, you can use this button to install the integration:
- AC200L (untested) - AC200L (untested)
- AC200M - AC200M
- AC300 (tested) - AC300 (tested)
- AC500 - AC500 (tested)
- EB3A - EB3A
- EP500 - EP500
- EP500P - EP500P
+2 -2
View File
@@ -38,9 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address = entry.data.get(CONF_ADDRESS) address = entry.data.get(CONF_ADDRESS)
device_name = entry.data.get(CONF_NAME) device_name = entry.data.get(CONF_NAME)
use_controls = entry.data.get(CONF_USE_CONTROLS) 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) 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) max_retries = entry.data.get(CONF_MAX_RETRIES, 5)
if address is None: if address is None:
@@ -86,6 +86,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
e_name = f"{device_info.get('name')} {name}" e_name = f"{device_info.get('name')} {name}"
self._address = address self._address = address
self._response_key = response_key self._response_key = response_key
self._unavailable_counter = 5
self._attr_device_info = device_info self._attr_device_info = device_info
self._attr_name = name self._attr_name = name
@@ -14,6 +14,8 @@ class ProtocolV1Device(BluettiDevice):
# Device info # Device info
self.struct.add_string_field("device_type", 10, 6) self.struct.add_string_field("device_type", 10, 6)
self.struct.add_sn_field("serial_number", 17) 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 # Power IO
self.struct.add_uint_field("dc_input_power", 36) self.struct.add_uint_field("dc_input_power", 36)
@@ -53,6 +53,8 @@ class DeviceReader:
if filter_registers is not None: if filter_registers is not None:
polling_commands = filter_registers polling_commands = filter_registers
pack_commands = [] 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 = {} parsed_data: dict = {}
@@ -66,8 +68,8 @@ class DeviceReader:
await self.client.connect() await self.client.connect()
break break
except Exception as e: except Exception as e:
if attempt == self.max_retries or attempt == 1: if attempt == self.max_retries:
raise # pass exception on max_retries attempt raise e # pass exception on max_retries attempt
else: else:
_LOGGER.warning( _LOGGER.warning(
f"Connect unsucessful (attempt {attempt}): {e}. Retrying..." 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) _LOGGER.warning("Pack polling failed (pack_num %i doesn't match expected %i)", set_pack, pack)
continue 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: for command in pack_commands:
# Request & parse result for each pack # Request & parse result for each pack
try: try:
@@ -136,8 +143,8 @@ class DeviceReader:
except ParseError: except ParseError:
_LOGGER.warning("Got a parse exception...") _LOGGER.warning("Got a parse exception...")
except TimeoutError: except TimeoutError as err:
_LOGGER.warning("Polling timed out. Trying again later") _LOGGER.error(f"Polling timed out ({self.polling_timeout}s). Trying again later", exc_info=err)
return None return None
except BleakError as err: except BleakError as err:
_LOGGER.error("Bleak error: %s", err) _LOGGER.error("Bleak error: %s", err)
@@ -28,6 +28,7 @@ class AC200L(ProtocolV1Device):
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls # Controls
self.struct.add_bool_field("power_off", 3060) self.struct.add_bool_field("power_off", 3060)
@@ -39,12 +40,9 @@ class AC200L(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers(
ReadHoldingRegisters(70, 5), filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
ReadHoldingRegisters(86, 3), )
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3060, 2),
]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
@@ -53,8 +51,6 @@ class AC200L(ProtocolV1Device):
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [ return [
ReadHoldingRegisters(91, 2), ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(96, 1), ReadHoldingRegisters(201, 2),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16)
] ]
@@ -28,6 +28,7 @@ class AC200M(ProtocolV1Device):
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls # Controls
self.struct.add_bool_field("power_off", 3060) self.struct.add_bool_field("power_off", 3060)
@@ -39,12 +40,9 @@ class AC200M(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers(
ReadHoldingRegisters(70, 5), filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
ReadHoldingRegisters(86, 3), )
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3060, 2),
]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
@@ -53,8 +51,6 @@ class AC200M(ProtocolV1Device):
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [ return [
ReadHoldingRegisters(91, 2), ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(96, 1), ReadHoldingRegisters(201, 2),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16)
] ]
@@ -54,17 +54,9 @@ class AC300(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers(
ReadHoldingRegisters(70, 11), filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
ReadHoldingRegisters(86, 3), )
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
@@ -76,9 +68,6 @@ class AC300(ProtocolV1Device):
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [ return [
ReadHoldingRegisters(91, 2), ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16),
ReadHoldingRegisters(201, 2), ReadHoldingRegisters(201, 2),
] ]
@@ -34,6 +34,7 @@ class AC500(ProtocolV1Device):
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field('pack_bms_version', 201)
# Controls # Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_enum_field("ups_mode", 3001, UpsMode)
@@ -49,21 +50,13 @@ class AC500(ProtocolV1Device):
@property @property
def pack_num_max(self): def pack_num_max(self):
return 4 return 6
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers(
ReadHoldingRegisters(70, 11), filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
ReadHoldingRegisters(86, 3), )
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
@@ -75,8 +68,6 @@ class AC500(ProtocolV1Device):
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [ return [
ReadHoldingRegisters(91, 2), ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(96, 1), ReadHoldingRegisters(201, 2),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16),
] ]
@@ -25,13 +25,7 @@ class EB3A(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers()
ReadHoldingRegisters(77, 1),
ReadHoldingRegisters(86, 1),
ReadHoldingRegisters(3034, 1),
ReadHoldingRegisters(3060, 1),
ReadHoldingRegisters(3063, 5),
]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
@@ -34,6 +34,7 @@ class EP500(ProtocolV1Device):
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls # Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_enum_field("ups_mode", 3001, UpsMode)
@@ -49,17 +50,9 @@ class EP500(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers(
ReadHoldingRegisters(70, 11), filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
ReadHoldingRegisters(86, 3), )
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
@@ -71,8 +64,6 @@ class EP500(ProtocolV1Device):
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [ return [
ReadHoldingRegisters(91, 2), ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(96, 1), ReadHoldingRegisters(201, 2),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16),
] ]
@@ -34,6 +34,7 @@ class EP500P(ProtocolV1Device):
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls # Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_enum_field("ups_mode", 3001, UpsMode)
@@ -49,17 +50,9 @@ class EP500P(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers(
ReadHoldingRegisters(70, 11), filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
ReadHoldingRegisters(86, 3), )
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
@@ -71,8 +64,6 @@ class EP500P(ProtocolV1Device):
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [ return [
ReadHoldingRegisters(91, 2), ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(96, 1), ReadHoldingRegisters(201, 2),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16),
] ]
@@ -97,28 +97,4 @@ class EP600(ProtocolV2Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return self.struct.get_read_holding_registers()
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),
]
@@ -125,6 +125,14 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
"ac_input_power": PowerFieldAttributes("AC Input Power"), "ac_input_power": PowerFieldAttributes("AC Input Power"),
"ac_output_power": PowerFieldAttributes("AC Output Power"), "ac_output_power": PowerFieldAttributes("AC Output Power"),
"dc_output_power": PowerFieldAttributes("DC 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"), "power_generation": EnergyFieldAttributes("Total Power Generation"),
"total_battery_percent": FieldAttributes( "total_battery_percent": FieldAttributes(
type=FieldType.NUMERIC, type=FieldType.NUMERIC,
@@ -4,6 +4,7 @@
from decimal import Decimal from decimal import Decimal
from enum import Enum from enum import Enum
from .commands import ReadHoldingRegisters
import struct import struct
from typing import Any, List, Optional, Tuple, Type from typing import Any, List, Optional, Tuple, Type
@@ -179,6 +180,35 @@ class DeviceStruct:
def add_sn_field(self, name: str, address: int): def add_sn_field(self, name: str, address: int):
self.fields.append(SerialNumberField(name, address)) 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: def parse(self, starting_address: int, data: bytes) -> dict:
# Offsets and size are counted in 2 byte chunks, so for the range we # Offsets and size are counted in 2 byte chunks, so for the range we
+2
View File
@@ -42,6 +42,8 @@ DIAGNOSTIC_FIELDS = [
"max_ac_output_power", "max_ac_output_power",
"max_ac_output_current", "max_ac_output_current",
"bcu_version", "bcu_version",
"dsp_version",
"arm_version",
"safety_module_version", "safety_module_version",
"high_voltage_module_version", "high_voltage_module_version",
"power_generation", "power_generation",