Compare commits

..
33 Commits
Author SHA1 Message Date
sredfern fb73daef62 Add support for EL30V2 device in manifest and device builder
HACS Action / HACS Action (push) Failing after 1m14s
Validate with hassfest / validate (push) Failing after 6s
Unittests / test (push) Failing after 37s
2025-08-04 23:23:53 +10:00
Patrick762 96a5cd4ee1 Merge pull request #174 from Patrick762/option-flow-update
Remove deprecated constructor in option flow
2025-06-14 21:16:32 +02:00
Patrick762 5204c165d8 bump version 2025-06-14 21:16:12 +02:00
Patrick762 869f80314b fix option flow 2025-06-14 21:12:02 +02:00
Patrick762 9ec0b539e1 remove deprecated constructor in option flow 2025-06-14 20:51:11 +02:00
Patrick762 8912a0bd08 Merge pull request #152 from Patrick762/unittest-fix
Unittest fix
2025-02-07 14:43:04 +01:00
Patrick762 db0147e7c1 added more requirements and dockerfile for tests 2025-02-07 14:30:42 +01:00
Patrick762 c5f985b5f6 Merge pull request #143 from Patrick762/unittests
added missing ac200l test
2024-10-25 17:00:03 +02:00
Patrick762 63f798ca5a added missing ac200l test 2024-10-25 16:58:29 +02:00
Patrick762 483ca4049d Merge pull request #142 from Patrick762/unittests
Unittest Action
2024-10-25 16:49:35 +02:00
Patrick762 de1ca78c90 fixed comment 2024-10-25 16:48:18 +02:00
Patrick762 a72f42eb4a added simple test for struct 2024-10-25 16:47:26 +02:00
Patrick762 6ce8f24477 added pip install requirements.txt 2024-10-25 16:32:19 +02:00
Patrick762 28cd715327 removed pip install 2024-10-25 16:30:50 +02:00
Patrick762 49c1409656 Update test.yaml 2024-10-25 16:29:00 +02:00
Patrick762 d61459c011 Create test.yaml 2024-10-25 16:23:21 +02:00
Patrick762 edf5e87720 Merge pull request #128 from Patrick762/version-update
update integration manifest version
2024-08-04 12:13:29 +02:00
Patrick762 742964b78b update integration manifest version 2024-08-04 12:12:55 +02:00
Patrick762 21e89e738d Merge pull request #127 from Kathou99/main
Update config_flow.py
2024-08-04 12:08:16 +02:00
Patrick762 52fd1932fa Merge pull request #118 from AChep/main
Various imprvements for AC300 & AC500
2024-08-04 12:07:43 +02:00
KathouQC(GirlsPowers) 3e54283faf Update config_flow.py
fix this bug: OptionsFlowHandler.async_step_init: Config entry f3a445b566f0876e0d79f02059b7cb2a has not been changed
2024-08-03 18:15:49 -04:00
Artem Chepurnyi 9be8aa47ac Add dsp/arm_version to diagnostic fields 2024-08-03 21:36:27 +03:00
Artem ChepurnyiandPatrick762 8967f3379c Fix a typo in the Polling commands text
Co-authored-by: Patrick762 <[email protected]>
2024-08-03 21:32:28 +03:00
Artem Chepurnyi da2017a24b chore: Mention that AC500 is tested 2024-06-30 17:14:30 +03:00
Artem Chepurnyi 83f0db267c Merge branch 'Patrick762:main' into main 2024-06-30 17:12:50 +03:00
Artem Chepurnyi fa9bcf63ca fix: Actually retry connecting to a device 2024-06-29 17:04:43 +03:00
Artem Chepurnyi 211f1971c5 chore: Remove .DS_Store from the repo 2024-06-29 16:48:09 +03:00
Artem Chepurnyi a1e9c180b3 improvement: Automatically build polling commands basing on the fields 2024-06-29 16:45:59 +03:00
Artem Chepurnyi 0e5c1a0b44 fix: Increase polling interval and timeout #109 2024-06-29 16:45:24 +03:00
Artem Chepurnyi 0f33a31ee5 fix: Correctly read battery packs' values - wait before setting the target pack number #109 2024-06-29 16:44:44 +03:00
Artem Chepurnyi 23551ae829 improvement: Add ARM and DSP versions 2024-06-29 16:42:32 +03:00
Artem Chepurnyi a54b36a90b fix: Initialize sensor. _unavailable_counter with a 5 2024-06-29 16:42:04 +03:00
Artem Chepurnyi f85661345c fix(AC500): Change max pack number to 6 2024-06-29 13:58:22 +03:00
28 changed files with 188 additions and 133 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
*/__pycache__/
*.pyc
.DS_Store
+22
View File
@@ -0,0 +1,22 @@
name: Unittests
on:
push:
pull_request:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run unittests
run: python3 -m unittest discover -s tests -p "*_test.py"
+1
View File
@@ -1,2 +1,3 @@
*/__pycache__/ */__pycache__/
*.pyc *.pyc
.DS_Store
+9
View File
@@ -0,0 +1,9 @@
FROM python:3
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY tests ./tests
COPY custom_components ./custom_components
RUN python3 -m unittest discover -s tests -p "*_test.py"
+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]:
@@ -0,0 +1,8 @@
"""Elite 30 V2 fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class EL30V2(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "EL30V2", sn)
@@ -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,
@@ -17,9 +17,10 @@ from ..devices.ep500p import EP500P
from ..devices.ep600 import EP600 from ..devices.ep600 import EP600
from ..devices.ep760 import EP760 from ..devices.ep760 import EP760
from ..devices.ep800 import EP800 from ..devices.ep800 import EP800
from ..devices.el30v2 import EL30V2
DEVICE_NAME_RE = re.compile( DEVICE_NAME_RE = re.compile(
r"^(AC60|AC70|AC70P|AC180|AC180P|AC200L|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" r"^(AC60|AC70|AC70P|AC180|AC180P|AC200L|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800|EL30V2)(\d+)$"
) )
@@ -55,6 +56,8 @@ def build_device(address: str, name: str):
return EP760(address, match[2]) return EP760(address, match[2])
if match[1] == "EP800": if match[1] == "EP800":
return EP800(address, match[2]) return EP800(address, match[2])
if match[1] == "EL30V2":
return EL30V2(address, match[2])
def get_type_by_bt_name(bt_name: str): def get_type_by_bt_name(bt_name: str):
@@ -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 -7
View File
@@ -130,7 +130,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry) -> None: def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry super().__init__()
async def async_step_init( async def async_step_init(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@@ -150,7 +150,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
if user_input[CONF_MAX_RETRIES] < 1: if user_input[CONF_MAX_RETRIES] < 1:
return self.async_abort(reason="invalid_retries") return self.async_abort(reason="invalid_retries")
changed = self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
self.config_entry, self.config_entry,
data={ data={
**self.config_entry.data, **self.config_entry.data,
@@ -163,11 +163,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
}, },
}, },
) )
if changed is False:
_LOGGER.error(
"Method OptionsFlowHandler.async_step_init: Config entry %s has not been changed",
self.config_entry.entry_id,
)
return self.async_create_entry( return self.async_create_entry(
title="", title="",
data={ data={
+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",
+3 -2
View File
@@ -14,7 +14,8 @@
{ "local_name": "EP600*" }, { "local_name": "EP600*" },
{ "local_name": "EP760*" }, { "local_name": "EP760*" },
{ "local_name": "EP800*" }, { "local_name": "EP800*" },
{ "local_name": "PBOX*" } { "local_name": "PBOX*" },
{ "local_name": "EL30V2*" }
], ],
"codeowners": ["@Patrick762"], "codeowners": ["@Patrick762"],
"config_flow": true, "config_flow": true,
@@ -24,5 +25,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues",
"requirements": [], "requirements": [],
"version": "0.1.3" "version": "0.1.5"
} }
+7 -1
View File
@@ -2,4 +2,10 @@ homeassistant
bleak_retry_connector bleak_retry_connector
pyserial pyserial
bluetooth_auto_recovery bluetooth_auto_recovery
crcmod crcmod
typing
asyncio
logging
bleak
async_timeout
aiousbwatcher
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
# Execute unittests of python library # Execute unittests of python library inside docker
python3 -m unittest discover -s tests -p "*_test.py" sudo docker build --progress=plain -t bluetti-bt-tests .
+9
View File
@@ -8,6 +8,7 @@ from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac70 import AC70
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac70p import AC70P from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac70p import AC70P
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180 import AC180 from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180 import AC180
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180p import AC180P from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180p import AC180P
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac200l import AC200L
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac200m import AC200M from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac200m import AC200M
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac300 import AC300 from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac300 import AC300
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac500 import AC500 from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac500 import AC500
@@ -66,6 +67,14 @@ class TestDeviceBuilder(unittest.TestCase):
self.assertIsInstance(built, AC180P) self.assertIsInstance(built, AC180P)
self.assertEqual(built.address, bt_addr) self.assertEqual(built.address, bt_addr)
def test_build_ac200l(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC200L56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC200L)
self.assertEqual(built.address, bt_addr)
def test_build_ac200m(self): def test_build_ac200m(self):
bt_addr = "aa:bb:cc:dd:ee:ff" bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC200M56786746478" bt_name = "AC200M56786746478"
+24
View File
@@ -0,0 +1,24 @@
"""Unittest for device struct."""
import unittest
from custom_components.bluetti_bt.bluetti_bt_lib.utils.commands import ReadHoldingRegisters
from custom_components.bluetti_bt.bluetti_bt_lib.utils.struct import DeviceStruct
class TestDeviceStruct(unittest.TestCase):
def test_get_read_holding_registers(self):
struct = DeviceStruct()
struct.add_swap_string_field("device_type", 110, 6)
struct.add_sn_field("serial_number", 116)
struct.add_decimal_field(
"power_generation", 154, 1
)
struct.add_uint_field("total_battery_percent", 102)
registers = struct.get_read_holding_registers()
self.assertGreater(len(registers), 0)
for reg in registers:
self.assertIsInstance(reg, ReadHoldingRegisters)