Compare commits

..
17 Commits
Author SHA1 Message Date
Patrick762 f8ed8141ac Merge pull request #71 from Patrick762/bugfixes
Bugfixes and improvements
2024-03-12 21:03:07 +01:00
Patrick762 bf86fe172b update version number 2024-03-12 20:56:13 +01:00
Patrick762 babdd1755e fix dc switch, added some switches for eb3a 2024-03-12 20:55:09 +01:00
Patrick762 7596f8cee1 fix ac_output_on switches 2024-03-12 20:25:24 +01:00
Patrick762 98d2a0b73e Merge pull request #72 from Patrick762/debug-logging
Debug logging
2024-03-12 20:20:37 +01:00
Patrick762 c499fbcd9b fix writable_ranges 2024-03-12 20:13:20 +01:00
Patrick762 ec996d5728 added debug logging to init 2024-03-12 20:12:39 +01:00
Patrick762 ec657ad59b Merge pull request #66 from Patrick762/ac180-ac180p
AC180 AC180P
2024-02-25 16:48:03 +01:00
Patrick762 df8375016c prepare next beta release 2024-02-25 16:46:14 +01:00
Patrick762 8901c11afc added unittest 2024-02-25 16:41:47 +01:00
Patrick762 da5443172b added ac180 and ac180p 2024-02-25 16:41:29 +01:00
Patrick762 85e50498f6 Merge pull request #64 from Patrick762/ep800
EP800
2024-02-25 15:40:30 +01:00
Patrick762 5fa1c8631f next beta release 2024-02-25 15:37:48 +01:00
Patrick762 9cba466228 basic ep800 support 2024-02-25 15:37:12 +01:00
Patrick762 cc42617bdb Merge pull request #63 from Patrick762/values-fix
Values fix
2024-02-25 15:08:50 +01:00
Patrick762 4d27f108fa fixed negative ac and dc solar values 2024-02-25 15:04:18 +01:00
Patrick762 6412630c85 added new int field 2024-02-25 15:03:27 +01:00
19 changed files with 219 additions and 24 deletions

No files matched your search

+4 -1
View File
@@ -17,6 +17,8 @@ After the installation, you can use this button to install the integration:
### Supported devices: ### Supported devices:
- AC60 - AC60
- AC180 (basic data)
- AC180P (basic data)
- AC200M - AC200M
- AC300 (tested) - AC300 (tested)
- AC500 - AC500
@@ -24,7 +26,8 @@ After the installation, you can use this button to install the integration:
- EP500 - EP500
- EP500P - EP500P
- EP600 (tested) - EP600 (tested)
- EP760 (beta, untested) - EP760 (basic data)
- EP800 (basic data)
### Available controls: ### Available controls:
If enabled in the Integration options (you need to reload the integration if you change this option): If enabled in the Integration options (you need to reload the integration if you change this option):
+6
View File
@@ -33,6 +33,8 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bluetti Powerstation from a config entry.""" """Set up Bluetti Powerstation from a config entry."""
_LOGGER.debug("Init Bluetti BT Integration")
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)
@@ -53,10 +55,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id].setdefault(DATA_POLLING_RUNNING, False) hass.data[DOMAIN][entry.entry_id].setdefault(DATA_POLLING_RUNNING, False)
# Create coordinator for polling # Create coordinator for polling
_LOGGER.debug("Creating coordinator")
coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistent_conn, polling_timeout, max_retries) coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistent_conn, polling_timeout, max_retries)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator) hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator)
_LOGGER.debug("Creating entities")
platforms: list = PLATFORMS platforms: list = PLATFORMS
if use_controls is True: if use_controls is True:
_LOGGER.warning("You are using controls with this integration at your own risk!") _LOGGER.warning("You are using controls with this integration at your own risk!")
@@ -65,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Setup platforms # Setup platforms
await hass.config_entries.async_forward_entry_setups(entry, platforms) await hass.config_entries.async_forward_entry_setups(entry, platforms)
_LOGGER.debug("Setup done")
return True return True
@@ -64,7 +64,7 @@ class ProtocolV1Device(BluettiDevice):
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return [range(3006, 3008)] return [range(3006, 3009)]
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
@@ -0,0 +1,8 @@
"""AC180 fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC180(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC180", sn)
@@ -0,0 +1,8 @@
"""AC180P fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC180P(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC180P", sn)
@@ -58,7 +58,7 @@ class AC300(ProtocolV1Device):
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3016),
3061, range(3061),
] ]
@property @property
@@ -57,7 +57,7 @@ class AC500(ProtocolV1Device):
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3016),
3061, range(3061),
] ]
@property @property
@@ -28,12 +28,15 @@ class EB3A(ProtocolV1Device):
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(77, 1), ReadHoldingRegisters(77, 1),
ReadHoldingRegisters(86, 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]:
return super().writable_ranges + [ return super().writable_ranges + [
3034, range(3034, 3035),
3060, range(3060, 3061),
range(3063, 3066), range(3063, 3067),
] ]
@@ -53,7 +53,7 @@ class EP500(ProtocolV1Device):
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3016),
3061, range(3061),
] ]
@property @property
@@ -53,7 +53,7 @@ class EP500P(ProtocolV1Device):
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3016),
3061, range(3061),
] ]
@property @property
@@ -20,11 +20,11 @@ class EP600(ProtocolV2Device):
# DC Solar # DC Solar
self.struct.add_uint_field("pv_input_power1", 1212) self.struct.add_uint_field("pv_input_power1", 1212)
self.struct.add_uint_field("pv_input_voltage1", 1213) self.struct.add_decimal_field("pv_input_voltage1", 1213, 1)
self.struct.add_uint_field("pv_input_current1", 1214) self.struct.add_decimal_field("pv_input_current1", 1214, 1)
self.struct.add_uint_field("pv_input_power2", 1220) self.struct.add_uint_field("pv_input_power2", 1220)
self.struct.add_uint_field("pv_input_voltage2", 1221) self.struct.add_decimal_field("pv_input_voltage2", 1221, 1)
self.struct.add_uint_field("pv_input_current2", 1222) self.struct.add_decimal_field("pv_input_current2", 1222, 1)
# ADL400 Smart Meter for AC Solar # ADL400 Smart Meter for AC Solar
self.struct.add_uint_field("adl400_ac_input_power_phase1", 1228) self.struct.add_uint_field("adl400_ac_input_power_phase1", 1228)
@@ -52,9 +52,9 @@ class EP600(ProtocolV2Device):
# EP600 AC Output # EP600 AC Output
self.struct.add_decimal_field("ac_output_frequency", 1500, 1) self.struct.add_decimal_field("ac_output_frequency", 1500, 1)
self.struct.add_uint_field("ac_output_power_phase1", 1510) self.struct.add_int_field("ac_output_power_phase1", 1510)
self.struct.add_uint_field("ac_output_power_phase2", 1517) self.struct.add_int_field("ac_output_power_phase2", 1517)
self.struct.add_uint_field("ac_output_power_phase3", 1524) self.struct.add_int_field("ac_output_power_phase3", 1524)
self.struct.add_decimal_field("ac_output_voltage_phase1", 1511, 1) self.struct.add_decimal_field("ac_output_voltage_phase1", 1511, 1)
self.struct.add_decimal_field("ac_output_voltage_phase2", 1518, 1) self.struct.add_decimal_field("ac_output_voltage_phase2", 1518, 1)
self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1) self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1)
@@ -0,0 +1,8 @@
"""EP800 fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class EP800(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "EP800", sn)
@@ -18,7 +18,6 @@ class FieldType(Enum):
NUMERIC = auto() NUMERIC = auto()
BOOL = auto() BOOL = auto()
ENUM = auto() ENUM = auto()
BUTTON = auto()
class FieldAttributes: class FieldAttributes:
@@ -220,7 +219,7 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
"total_grid_feed": EnergyFieldAttributes("Total Grid Feed"), "total_grid_feed": EnergyFieldAttributes("Total Grid Feed"),
# Device specific controls # Device specific controls
"power_off": FieldAttributes( "power_off": FieldAttributes(
type=FieldType.BUTTON, type=FieldType.BOOL,
setter=True, setter=True,
name="Power Off", name="Power Off",
), ),
@@ -294,7 +293,7 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
), ),
"power_lifting_on": FieldAttributes( "power_lifting_on": FieldAttributes(
type=FieldType.BOOL, type=FieldType.BOOL,
setter=False, # Disabled for safety reasons setter=True,
name="Power Lifting", name="Power Lifting",
), ),
} }
@@ -3,6 +3,8 @@
import re import re
from ..devices.ac60 import AC60 from ..devices.ac60 import AC60
from ..devices.ac180 import AC180
from ..devices.ac180p import AC180P
from ..devices.ac200m import AC200M from ..devices.ac200m import AC200M
from ..devices.ac300 import AC300 from ..devices.ac300 import AC300
from ..devices.ac500 import AC500 from ..devices.ac500 import AC500
@@ -11,9 +13,10 @@ from ..devices.ep500 import EP500
from ..devices.ep500p import EP500P 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
DEVICE_NAME_RE = re.compile( DEVICE_NAME_RE = re.compile(
r"^(AC60|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760)(\d+)$" r"^(AC60|AC180|AC180P|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$"
) )
@@ -21,6 +24,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] == "AC180":
return AC180(address, match[2])
if match[1] == "AC180P":
return AC180P(address, match[2])
if match[1] == "AC200M": if match[1] == "AC200M":
return AC200M(address, match[2]) return AC200M(address, match[2])
if match[1] == "AC300": if match[1] == "AC300":
@@ -37,6 +44,8 @@ def build_device(address: str, name: str):
return EP600(address, match[2]) return EP600(address, match[2])
if match[1] == "EP760": if match[1] == "EP760":
return EP760(address, match[2]) return EP760(address, match[2])
if match[1] == "EP800":
return EP800(address, match[2])
def get_type_by_bt_name(bt_name: str): def get_type_by_bt_name(bt_name: str):
@@ -44,6 +44,21 @@ class UintField(DeviceField):
return val >= self.range[0] and val <= self.range[1] return val >= self.range[0] and val <= self.range[1]
class IntField(DeviceField):
def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]]):
self.range = range
super().__init__(name, address, 1)
def parse(self, data: bytes) -> int:
return struct.unpack(">h", data)[0]
def in_range(self, val: int) -> bool:
if self.range is None:
return True
else:
return val >= self.range[0] and val <= self.range[1]
class BoolField(DeviceField): class BoolField(DeviceField):
def __init__(self, name: str, address: int): def __init__(self, name: str, address: int):
super().__init__(name, address, 1) super().__init__(name, address, 1)
@@ -132,6 +147,9 @@ class DeviceStruct:
def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None): def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None):
self.fields.append(UintField(name, address, range)) self.fields.append(UintField(name, address, range))
def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None):
self.fields.append(IntField(name, address, range))
def add_bool_field(self, name: str, address: int): def add_bool_field(self, name: str, address: int):
self.fields.append(BoolField(name, address)) self.fields.append(BoolField(name, address))
+5 -2
View File
@@ -26,8 +26,11 @@ SUPPORTED_MODELS = [
] ]
CONTROL_FIELDS = [ CONTROL_FIELDS = [
"ac_output_on", "ac_output_on_switch",
"dc_output_on", "dc_output_on_switch",
"power_off",
"eco_on",
"power_lifting_on",
] ]
DIAGNOSTIC_FIELDS = [ DIAGNOSTIC_FIELDS = [
+4 -2
View File
@@ -2,14 +2,16 @@
"domain": "bluetti_bt", "domain": "bluetti_bt",
"name": "Bluetti BT", "name": "Bluetti BT",
"bluetooth": [ "bluetooth": [
{ "local_name": "AC60*" },
{ "local_name": "AC180*" },
{ "local_name": "AC200M*" }, { "local_name": "AC200M*" },
{ "local_name": "AC300*" }, { "local_name": "AC300*" },
{ "local_name": "AC500*" }, { "local_name": "AC500*" },
{ "local_name": "AC60*" },
{ "local_name": "EB3A*" }, { "local_name": "EB3A*" },
{ "local_name": "EP500*" }, { "local_name": "EP500*" },
{ "local_name": "EP600*" }, { "local_name": "EP600*" },
{ "local_name": "EP760*" }, { "local_name": "EP760*" },
{ "local_name": "EP800*" },
{ "local_name": "PBOX*" } { "local_name": "PBOX*" }
], ],
"codeowners": ["@Patrick762"], "codeowners": ["@Patrick762"],
@@ -20,5 +22,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.0.17" "version": "0.0.20"
} }
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Execute unittests of python library
python3 -m unittest discover -s tests -p "*_test.py"
+124
View File
@@ -0,0 +1,124 @@
"""Unittest for device builder."""
import unittest
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.ac180 import AC180
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180p import AC180P
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.ac500 import AC500
from custom_components.bluetti_bt.bluetti_bt_lib.devices.eb3a import EB3A
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ep500 import EP500
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ep500p import EP500P
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ep600 import EP600
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ep760 import EP760
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ep800 import EP800
class TestDeviceBuilder(unittest.TestCase):
def test_build_Unknow(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "PBOX56786746478"
with self.assertRaises(TypeError):
build_device(bt_addr, bt_name)
def test_build_ac60(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC6056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC60)
self.assertEqual(built.address, bt_addr)
def test_build_ac180(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC18056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC180)
self.assertEqual(built.address, bt_addr)
def test_build_ac180p(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC180P56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC180P)
self.assertEqual(built.address, bt_addr)
def test_build_ac200m(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC200M56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC200M)
self.assertEqual(built.address, bt_addr)
def test_build_ac300(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC30056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC300)
self.assertEqual(built.address, bt_addr)
def test_build_ac500(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC50056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC500)
self.assertEqual(built.address, bt_addr)
def test_build_eb3a(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "EB3A56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, EB3A)
self.assertEqual(built.address, bt_addr)
def test_build_ep500(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "EP50056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, EP500)
self.assertEqual(built.address, bt_addr)
def test_build_ep500p(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "EP500P56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, EP500P)
self.assertEqual(built.address, bt_addr)
def test_build_EP600(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "EP60056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, EP600)
self.assertEqual(built.address, bt_addr)
def test_build_EP760(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "EP76056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, EP760)
self.assertEqual(built.address, bt_addr)
def test_build_EP800(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "EP80056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, EP800)
self.assertEqual(built.address, bt_addr)
if __name__ == '__main__':
unittest.main()