Compare commits

...
59 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
Patrick762 6f9afedfdb Merge pull request #119 from kvanonavk/main
AC180P Advanced Integration
2024-07-19 16:17:16 +02:00
Patrick762 fe23dd00cd Merge pull request #122 from Patrick762/ac70-fix
Added ac70 auto detection
2024-07-19 16:13:48 +02:00
Patrick762 566761dba7 update version 2024-07-19 16:13:19 +02:00
Patrick762 33cb6e6fbd added ac70 auto detection 2024-07-19 15:02:57 +02:00
kvanonavk 5823136280 Merge branch 'Patrick762:main' into main 2024-07-05 14:44:12 +03:00
kvanonavk 7b6f845edb fixes 2024-07-05 14:42:56 +03:00
kvanonavk 1fbcf83b1b simple set charging mode (no turbo) 2024-07-01 18:16:58 +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
Patrick762 149633f9c8 Increase version number 2024-06-30 15:04:23 +02:00
Patrick762 9ab7cefda7 Merge pull request #116 from matthewpucc/main
Support AC70 and AC70P Device
2024-06-30 15:03:35 +02:00
Patrick762 52853d6d3e Merge pull request #117 from rotx/fix-multiply
Fix multiplication of Decimal with float
2024-06-30 15:03:14 +02:00
kvanonavk c96f551024 fixes 2024-06-30 14:11:31 +03:00
kvanonavk 75209ee1aa readme fix 2 2024-06-30 13:59:45 +03:00
kvanonavk 7396616113 readme fix 2024-06-30 13:56:58 +03:00
kvanonavk a7f7ff1843 fixes 2024-06-30 13:55:10 +03:00
kvanonavk cc755b3003 test + fix 2024-06-30 13:25:39 +03:00
kvanonavk e5f690ef3b AC180P more data and functionality 2024-06-30 12:24:52 +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
Robert Muchsel 3d3401c942 Fix multiplication of Decimal with float 2024-06-21 20:52:48 -05:00
Matthew Puccerella f50232ee89 Merge pull request #1 from matthewpucc/support_ac70_devices
Add support AC70 and AC70P devices
2024-06-20 16:12:53 -07:00
Matthew Puccerella 5922362772 Add support AC70 and AC70P devices 2024-06-20 23:11:47 +00:00
Patrick762 6478825503 Merge pull request #107 from Patrick762/next
Fixed values and improved unavailable handling
2024-06-02 14:06:20 +02:00
Patrick762 3169ee182e increase version number 2024-06-02 14:03:02 +02:00
Patrick762 ece1fc4129 only set unavailable after 5 retries 2024-06-02 13:58:21 +02:00
Patrick762 4d54e048c5 ignore packs not connected 2024-06-02 13:49:18 +02:00
Patrick762 09a9112279 check pack_num after setting it 2024-06-02 13:48:45 +02:00
Patrick762 b37276ec52 updated ac200l sensors based on feedback 2024-06-02 13:12:43 +02:00
Patrick762 4f88792371 Update bug_report.yaml 2024-05-31 17:16:26 +02:00
33 changed files with 370 additions and 170 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
*/__pycache__/
*.pyc
.DS_Store
+2 -13
View File
@@ -14,21 +14,10 @@ body:
value: "An exception appeared" value: "An exception appeared"
validations: validations:
required: true required: true
- type: dropdown - type: textarea
id: version id: version
attributes: attributes:
label: Version label: What version of our software are you running?
description: What version of our software are you running?
options:
- 0.0.17
- 0.0.16
- 0.0.15
- 0.0.14
- 0.0.13
- 0.0.11
- 0.0.10
- 0.0.9
default: 0
validations: validations:
required: true required: true
- type: textarea - type: textarea
+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"
+4 -2
View File
@@ -17,12 +17,14 @@ After the installation, you can use this button to install the integration:
### Supported devices: ### Supported devices:
- AC60 - AC60
- AC70 (basic data)
- AC70P (untested)
- AC180 (basic data) - AC180 (basic data)
- AC180P (basic data) - AC180P (tested)
- 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:
+33 -7
View File
@@ -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
@@ -97,6 +98,27 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
"""Return if entity is available.""" """Return if entity is available."""
return self._attr_available return self._attr_available
def _set_available(self):
"""Set sensor as available."""
self._attr_available = True
self._unavailable_counter = 0
self._attr_extra_state_attributes = {}
self.async_write_ha_state()
def _set_unavailable(self, cause: str = "Unknown"):
"""Set sensor as unavailable."""
self._unavailable_counter += 1
self._attr_extra_state_attributes = {
"unavailable_counter": self._unavailable_counter,
"unavailable_cause": cause,
}
if self._unavailable_counter >= 5:
self._attr_available = False
self.async_write_ha_state()
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
@@ -104,19 +126,24 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected: if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected:
return return
if self.coordinator.data is None:
_LOGGER.debug(
"Data from coordinator is None",
)
self._set_unavailable("Data is None")
return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
if not isinstance(self.coordinator.data, dict): if not isinstance(self.coordinator.data, dict):
_LOGGER.debug( _LOGGER.debug(
"Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id) "Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False self._set_unavailable("Invalid data")
self.async_write_ha_state()
return return
response_data = self.coordinator.data.get(self._response_key) response_data = self.coordinator.data.get(self._response_key)
if response_data is None: if response_data is None:
self._attr_available = False self._set_unavailable("No data")
self.async_write_ha_state()
return return
if not isinstance(response_data, bool): if not isinstance(response_data, bool):
@@ -125,10 +152,9 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
unique_id_loggable(self._attr_unique_id), unique_id_loggable(self._attr_unique_id),
response_data, response_data,
) )
self._attr_available = False self._set_unavailable("Invalid data type")
self.async_write_ha_state()
return return
self._attr_available = True self._set_available()
self._attr_is_on = self.coordinator.data[self._response_key] is True self._attr_is_on = self.coordinator.data[self._response_key] is True
self.async_write_ha_state() self.async_write_ha_state()
@@ -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..."
@@ -110,7 +112,17 @@ class DeviceReader:
await self._async_send_command(command) await self._async_send_command(command)
) )
_LOGGER.debug("Raw data set: %s", body) _LOGGER.debug("Raw data set: %s", body)
# b'\x00\x02' b'\x00\x03' here (seems like result)
# Check set pack_num
set_pack = int.from_bytes(body, byteorder='big')
if set_pack is not pack:
_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: for command in pack_commands:
# Request & parse result for each pack # Request & parse result for each pack
@@ -124,13 +136,15 @@ class DeviceReader:
_LOGGER.debug("Parsed data: %s", parsed) _LOGGER.debug("Parsed data: %s", parsed)
for key, value in parsed.items(): for key, value in parsed.items():
# Ignore likely unavailable pack data
if value != 0:
parsed_data.update({key + str(pack): value}) parsed_data.update({key + str(pack): value})
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)
@@ -1,8 +1,52 @@
"""AC180P fields.""" """AC180P fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device from typing import List
from ..field_enums import ChargingMode
from ..utils.commands import ReadHoldingRegisters
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC180P(ProtocolV2Device): class AC180P(ProtocolV2Device):
def __init__(self, address: str, sn: str): def __init__(self, address: str, sn: str):
super().__init__(address, "AC180P", sn) super().__init__(address, "AC180P", sn)
# Power IO
self.struct.add_uint_field('dc_output_power', 140)
self.struct.add_uint_field('ac_output_power', 142)
self.struct.add_uint_field('dc_input_power', 144)
self.struct.add_uint_field('ac_input_power', 146)
# Input Details (1100 - 1300)
self.struct.add_decimal_field('ac_input_voltage', 1314, 1)
# Controls (2000)
self.struct.add_bool_field('ac_output_on_switch', 2011)
self.struct.add_bool_field('dc_output_on_switch', 2012)
self.struct.add_bool_field('silent_charging_on', 2020) # to be able to choose between normal and silent charging modes
self.struct.add_enum_field('charging_mode', 2020, ChargingMode) # can't be set from HA, custom switch needed
self.struct.add_bool_field('power_lifting_on', 2021)
# Controls (2200)
self.struct.add_bool_field('grid_enhancement_mode_on', 2225)
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(140, 1),
ReadHoldingRegisters(142, 1),
ReadHoldingRegisters(144, 1),
ReadHoldingRegisters(146, 1),
ReadHoldingRegisters(1314, 1),
ReadHoldingRegisters(2011, 1),
ReadHoldingRegisters(2012, 1),
ReadHoldingRegisters(2020, 1),
ReadHoldingRegisters(2021, 1),
ReadHoldingRegisters(2225, 1),
]
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [
range(2000, 2022),
range(2200, 2226)
]
@@ -13,21 +13,22 @@ class AC200L(ProtocolV1Device):
# Details # Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) self.struct.add_enum_field("ac_output_mode", 70, OutputMode)
self.struct.add_decimal_field("internal_ac_voltage", 71, 1) self.struct.add_decimal_field("internal_ac_voltage", 71, 1, multiplier=10)
self.struct.add_decimal_field("internal_current_one", 72, 1) self.struct.add_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73) self.struct.add_uint_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_decimal_field("internal_ac_frequency", 74, 2, multiplier=10)
self.struct.add_uint_field("internal_dc_input_voltage", 86) self.struct.add_uint_field("internal_dc_input_voltage", 86, multiplier=.1)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1, multiplier=10)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2) self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
# Battery packs # Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1) self.struct.add_decimal_field("total_battery_voltage", 92, 1, multiplier=.1)
self.struct.add_uint_field("pack_num_result", 96) # internal self.struct.add_uint_field("pack_num_result", 96) # internal
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),
] ]
@@ -0,0 +1,8 @@
"""AC70 fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC70(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC70", sn)
@@ -0,0 +1,8 @@
"""AC70P fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC70P(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC70P", sn)
@@ -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,
@@ -316,6 +324,16 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
setter=True, setter=True,
name="Power Lifting", name="Power Lifting",
), ),
"grid_enhancement_mode_on": FieldAttributes(
type=FieldType.BOOL,
setter=True,
name="Grid Enhancement Mode",
),
"silent_charging_on": FieldAttributes(
type=FieldType.BOOL,
setter=True,
name="Silent Charging",
),
} }
@@ -3,6 +3,8 @@
import re import re
from ..devices.ac60 import AC60 from ..devices.ac60 import AC60
from ..devices.ac70 import AC70
from ..devices.ac70p import AC70P
from ..devices.ac180 import AC180 from ..devices.ac180 import AC180
from ..devices.ac180p import AC180P from ..devices.ac180p import AC180P
from ..devices.ac200l import AC200L from ..devices.ac200l import AC200L
@@ -15,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|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+)$"
) )
@@ -25,6 +28,10 @@ def build_device(address: str, name: str):
match = DEVICE_NAME_RE.match(name) match = DEVICE_NAME_RE.match(name)
if match[1] == "AC60": if match[1] == "AC60":
return AC60(address, match[2]) return AC60(address, match[2])
if match[1] == "AC70":
return AC70(address, match[2])
if match[1] == "AC70P":
return AC70P(address, match[2])
if match[1] == "AC180": if match[1] == "AC180":
return AC180(address, match[2]) return AC180(address, match[2])
if match[1] == "AC180P": if match[1] == "AC180P":
@@ -49,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
@@ -91,7 +92,7 @@ class DecimalField(DeviceField):
def parse(self, data: bytes) -> Decimal: def parse(self, data: bytes) -> Decimal:
val = Decimal(struct.unpack("!H", data)[0]) val = Decimal(struct.unpack("!H", data)[0])
return (val / 10 ** self.scale) * self.multiplier return (val / 10 ** self.scale) * Decimal(self.multiplier)
def in_range(self, val: Decimal) -> bool: def in_range(self, val: Decimal) -> bool:
if self.range is None: if self.range is None:
@@ -180,6 +181,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
# need to divide the byte size by 2 # need to divide the byte size by 2
+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={
+6
View File
@@ -23,6 +23,7 @@ SUPPORTED_MODELS = [
"EP500P", "EP500P",
"EP600", "EP600",
"EP760", "EP760",
"AC180P",
] ]
CONTROL_FIELDS = [ CONTROL_FIELDS = [
@@ -31,6 +32,8 @@ CONTROL_FIELDS = [
"power_off", "power_off",
"eco_on", "eco_on",
"power_lifting_on", "power_lifting_on",
"grid_enhancement_mode_on",
"silent_charging_on",
] ]
DIAGNOSTIC_FIELDS = [ DIAGNOSTIC_FIELDS = [
@@ -39,10 +42,13 @@ 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",
"total_ac_consumption", "total_ac_consumption",
"total_grid_consumption", "total_grid_consumption",
"total_grid_feed", "total_grid_feed",
"ac_input_voltage",
] ]
+4 -2
View File
@@ -3,6 +3,7 @@
"name": "Bluetti BT", "name": "Bluetti BT",
"bluetooth": [ "bluetooth": [
{ "local_name": "AC60*" }, { "local_name": "AC60*" },
{ "local_name": "AC70*" },
{ "local_name": "AC180*" }, { "local_name": "AC180*" },
{ "local_name": "AC200L*" }, { "local_name": "AC200L*" },
{ "local_name": "AC200M*" }, { "local_name": "AC200M*" },
@@ -13,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,
@@ -23,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.0" "version": "0.1.5"
} }
+27 -9
View File
@@ -114,6 +114,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
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 = 0
self._attr_device_info = device_info self._attr_device_info = device_info
self._attr_name = name self._attr_name = name
@@ -130,6 +131,27 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
"""Return if entity is available.""" """Return if entity is available."""
return self._attr_available return self._attr_available
def _set_available(self):
"""Set sensor as available."""
self._attr_available = True
self._unavailable_counter = 0
self._attr_extra_state_attributes = {}
self.async_write_ha_state()
def _set_unavailable(self, cause: str = "Unknown"):
"""Set sensor as unavailable."""
self._unavailable_counter += 1
self._attr_extra_state_attributes = {
"unavailable_counter": self._unavailable_counter,
"unavailable_cause": cause,
}
if self._unavailable_counter >= 5:
self._attr_available = False
self.async_write_ha_state()
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
@@ -141,8 +163,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
_LOGGER.debug( _LOGGER.debug(
"Data from coordinator is None", "Data from coordinator is None",
) )
self._attr_available = False self._set_unavailable("Data is None")
self.async_write_ha_state()
return return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
@@ -150,15 +171,13 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
_LOGGER.warning( _LOGGER.warning(
"Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id) "Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False self._set_unavailable("Invalid data")
self.async_write_ha_state()
return return
response_data = self.coordinator.data.get(self._response_key) response_data = self.coordinator.data.get(self._response_key)
if response_data is None: if response_data is None:
_LOGGER.debug("No data for available for (%s)", self._response_key) _LOGGER.debug("No data for available for (%s)", self._response_key)
self._attr_available = False self._set_unavailable("No data")
self.async_write_ha_state()
return return
if ( if (
@@ -174,11 +193,10 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
response_data, response_data,
type(response_data), type(response_data),
) )
self._attr_available = False self._set_unavailable("Invalid data type")
self.async_write_ha_state()
return return
self._attr_available = True self._set_available()
# Different for enum and numeric # Different for enum and numeric
if ( if (
+11
View File
@@ -0,0 +1,11 @@
homeassistant
bleak_retry_connector
pyserial
bluetooth_auto_recovery
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 .
+27
View File
@@ -4,8 +4,11 @@ import unittest
from custom_components.bluetti_bt.bluetti_bt_lib.utils.device_builder import build_device from custom_components.bluetti_bt.bluetti_bt_lib.utils.device_builder import build_device
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac60 import AC60 from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac60 import AC60
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.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
@@ -32,6 +35,22 @@ class TestDeviceBuilder(unittest.TestCase):
self.assertIsInstance(built, AC60) self.assertIsInstance(built, AC60)
self.assertEqual(built.address, bt_addr) self.assertEqual(built.address, bt_addr)
def test_build_ac70(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC7056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC70)
self.assertEqual(built.address, bt_addr)
def test_build_ac70p(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC70P56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC70P)
self.assertEqual(built.address, bt_addr)
def test_build_ac180(self): def test_build_ac180(self):
bt_addr = "aa:bb:cc:dd:ee:ff" bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC18056786746478" bt_name = "AC18056786746478"
@@ -48,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)