From 66a7de90510f3024b4aef6433fc282d01d0b953b Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 6 Jan 2024 15:39:08 +0100 Subject: [PATCH 01/82] added has_entity_name for compatibility reasons --- custom_components/bluetti_bt/binary_sensor.py | 1 + custom_components/bluetti_bt/sensor.py | 1 + custom_components/bluetti_bt/switch.py | 1 + 3 files changed, 3 insertions(+) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index a52a269..807ceb4 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -91,6 +91,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): """Init battery entity.""" super().__init__(coordinator) + self._attr_has_entity_name = True e_name = f"{device_info.get('name')} {name}" self._address = address self._response_key = response_key diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 428800e..5f930e8 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -122,6 +122,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): """Init battery entity.""" super().__init__(coordinator) + self._attr_has_entity_name = True e_name = f"{device_info.get('name')} {name}" self._address = address self._response_key = response_key diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 62280e4..c244afd 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -108,6 +108,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): self._entry_id = entry_id self._attr_device_info = device_info + self._attr_has_entity_name = True self._attr_name = e_name self._attr_available = False self._attr_unique_id = get_unique_id(e_name) From a8931e09c73a5ca25830ecfd23de1a7ee69e21ef Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 6 Jan 2024 15:46:04 +0100 Subject: [PATCH 02/82] added disclaimer --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ce86d3d..155d57f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Bluetti Integration for Home Assistant Based on [bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt). +## Disclaimer +This integration is provided without any warranty or support by Bluetti (unfortunately). I do not take responsibility for any problems it may cause in all cases. Use it at your own risk. + ## Installation To install this integration, you first need [HACS](https://hacs.xyz/) installed. After the installation, you can use this button to install the integration: From 8eb09a32f00355665e98764e14e4141a3182f768 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 3 Feb 2024 17:57:49 +0100 Subject: [PATCH 03/82] added ep700, replaced dummy device --- README.md | 1 + custom_components/bluetti_bt/__init__.py | 16 +++ custom_components/bluetti_bt/binary_sensor.py | 6 +- custom_components/bluetti_bt/const.py | 4 + custom_components/bluetti_bt/coordinator.py | 100 +--------------- custom_components/bluetti_bt/devices/ep600.py | 113 ++++++++++++++++++ custom_components/bluetti_bt/devices/ep700.py | 67 +++++++++++ custom_components/bluetti_bt/manifest.json | 5 +- custom_components/bluetti_bt/sensor.py | 6 +- custom_components/bluetti_bt/switch.py | 6 +- 10 files changed, 212 insertions(+), 112 deletions(-) create mode 100644 custom_components/bluetti_bt/devices/ep600.py create mode 100644 custom_components/bluetti_bt/devices/ep700.py diff --git a/README.md b/README.md index ce86d3d..d02c746 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ After the installation, you can use this button to install the integration: - EP500 - EP500P - EP600 (tested) +- EP700 (beta, untested) ### Available sensors: All sensors which are available in bluetti_mqtt (Based on [this file](https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/mqtt_client.py)). diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 8b69d58..5aeee6b 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -12,6 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.exceptions import ConfigEntryNotReady +from bluetti_mqtt.bluetooth import build_device as orig_build_device + from .const import ( CONF_MAX_RETRIES, CONF_PERSISTENT_CONN, @@ -20,10 +22,13 @@ from .const import ( CONF_USE_CONTROLS, DATA_COORDINATOR, DATA_POLLING_RUNNING, + DEVICE_NAME_RE, DOMAIN, MANUFACTURER, ) from .coordinator import PollingCoordinator +from .devices.ep600 import EP600 +from .devices.ep700 import EP700 PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -102,4 +107,15 @@ def get_type_by_bt_name(bt_name: str): dev_type = "EP500" elif bt_name.startswith("EP600"): dev_type = "EP600" + elif bt_name.startswith("EP700"): + dev_type = "EP700" return dev_type + +def build_device(address: str, name: str): + match = DEVICE_NAME_RE.match(name) + if match[1] == 'EP700': + return EP700(address, match[2]) + elif match[1] == 'EP600': + return EP600(address, match[2]) + else: + return orig_build_device(address, name) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index a52a269..a508f31 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -18,16 +18,15 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) -from bluetti_mqtt.bluetooth import build_device from bluetti_mqtt.mqtt_client import ( NORMAL_DEVICE_FIELDS, DC_INPUT_FIELDS, MqttFieldType, ) -from . import device_info as dev_info, get_unique_id +from . import device_info as dev_info, get_unique_id, build_device from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS -from .coordinator import PollingCoordinator, DummyDevice +from .coordinator import PollingCoordinator from .utils import unique_id_loggable _LOGGER = logging.getLogger(__name__) @@ -49,7 +48,6 @@ async def async_setup_entry( # Add sensors according to device_info bluetti_device = build_device(address, device_name) - bluetti_device = DummyDevice(bluetti_device) sensors_to_add = [] all_fields = NORMAL_DEVICE_FIELDS diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 75c2406..e25a033 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -1,5 +1,7 @@ """Constants for the Bluetti BT integration.""" +import re + from bluetti_mqtt.mqtt_client import MqttFieldConfig, MqttFieldType DOMAIN = "bluetti_bt" @@ -15,6 +17,8 @@ CONF_MAX_RETRIES = "max_retries" DATA_COORDINATOR = "coordinator" DATA_POLLING_RUNNING = "polling_running" +DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|EP500P|EP500|EP600|EP700|EB3A)(\d+)$') + CONTROL_FIELDS = [ "ac_output_on", "dc_output_on", diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 8239c64..304e1e7 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -21,110 +21,17 @@ from bluetti_mqtt.bluetooth import ( BadConnectionError, ModbusError, ParseError, - build_device, ) from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice from bluetti_mqtt.core.commands import ReadHoldingRegisters +from . import build_device from .const import DATA_POLLING_RUNNING, DOMAIN from .utils import mac_loggable _LOGGER = logging.getLogger(__name__) -class DummyDevice(BluettiDevice): - """Dummy device used to add more fields to existing devices. - - Changes made here are only temporary and should also be - contributed to https://github.com/warhammerkid/bluetti_mqtt - """ - - def __init__(self, device: BluettiDevice): - """Init dummy device with real device.""" - self.struct = device.struct - - if device.type == "EP600": - # DC Solar Input (copied from PR https://github.com/warhammerkid/bluetti_mqtt/pull/87 by KM011092) - self.struct.add_uint_field('pv_input_power1', 1212) # MPP 1 in - value * 0.1 - #self.struct.add_uint_field('pv_input_voltage1', 1213) # MPP 1 in - value * 0.1 - #self.struct.add_uint_field('pv_input_current1', 1214) # MPP 1 in - self.struct.add_uint_field('pv_input_power2', 1220) # MPP 2 in - value * 0.1 - #self.struct.add_uint_field('pv_input_voltage2', 1221) # MPP 2 in - value * 0.1 - #self.struct.add_uint_field('pv_input_current2', 1222) # MPP 2 in - # 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_phase2", 1236) - self.struct.add_uint_field("adl400_ac_input_power_phase3", 1244) - self.struct.add_decimal_field("adl400_ac_input_voltage_phase1", 1229, 1) - self.struct.add_decimal_field("adl400_ac_input_voltage_phase2", 1237, 1) - self.struct.add_decimal_field("adl400_ac_input_voltage_phase3", 1245, 1) - # Grid Input - self.struct.add_decimal_field("grid_input_frequency", 1300, 1) - self.struct.add_uint_field("grid_input_power_phase1", 1313) - self.struct.add_uint_field("grid_input_power_phase2", 1319) - self.struct.add_uint_field("grid_input_power_phase3", 1325) - self.struct.add_decimal_field("grid_input_voltage_phase1", 1314, 1) - self.struct.add_decimal_field("grid_input_voltage_phase2", 1320, 1) - self.struct.add_decimal_field("grid_input_voltage_phase3", 1326, 1) - self.struct.add_decimal_field("grid_input_current_phase1", 1315, 1) - self.struct.add_decimal_field("grid_input_current_phase2", 1321, 1) - self.struct.add_decimal_field("grid_input_current_phase3", 1327, 1) - # EP600 AC Output - self.struct.add_decimal_field("ac_output_frequency", 1500, 1) - self.struct.add_uint_field("ac_output_power_phase1", 1510) - self.struct.add_uint_field("ac_output_power_phase2", 1517) - self.struct.add_uint_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_phase2", 1518, 1) - self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1) - self.struct.add_decimal_field("ac_output_current_phase1", 1512, 1) - self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1) - self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1) - - super().__init__(device.address, device.type, device.sn) - self._parent = device - - @property - def pack_num_max(self): - return self._parent.pack_num_max - - @property - def polling_commands(self) -> List[ReadHoldingRegisters]: - if self.type == "EP600": - return [ - ReadHoldingRegisters(100, 62), - ReadHoldingRegisters(1228, 19), - ReadHoldingRegisters(1300, 28), - ReadHoldingRegisters(1500, 27), - ReadHoldingRegisters(2022, 6), - ReadHoldingRegisters(2213, 4), - ReadHoldingRegisters(1212, 11), - # Battery - ReadHoldingRegisters(6101, 7), - ReadHoldingRegisters(6175, 11), - ] - - return self._parent.polling_commands - - @property - def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - if self.type == "EP600": - return [] - return self._parent.pack_polling_commands - - @property - def logging_commands(self) -> List[ReadHoldingRegisters]: - return self._parent.logging_commands - - @property - def pack_logging_commands(self) -> List[ReadHoldingRegisters]: - return self._parent.pack_logging_commands - - @property - def writable_ranges(self) -> List[range]: - return self._parent.writable_ranges - - class PollingCoordinator(DataUpdateCoordinator): """Polling coordinator.""" @@ -150,14 +57,11 @@ class PollingCoordinator(DataUpdateCoordinator): self.notify_future = None self.current_command = None self.notify_response = bytearray() - bluetti_device = build_device(address, device_name) + self.bluetti_device = build_device(address, device_name) self.persistent_conn = persistent_conn self.polling_timeout = polling_timeout self.max_retries = max_retries - # Add or modify device fields - self.bluetti_device = DummyDevice(bluetti_device) - # Create client self.logger.debug("Creating client") device = bluetooth.async_ble_device_from_address(hass, address) diff --git a/custom_components/bluetti_bt/devices/ep600.py b/custom_components/bluetti_bt/devices/ep600.py new file mode 100644 index 0000000..3ea91a7 --- /dev/null +++ b/custom_components/bluetti_bt/devices/ep600.py @@ -0,0 +1,113 @@ +"""EP600 Field definitions. + +Changes made here are only temporary and should also be +contributed to https://github.com/warhammerkid/bluetti_mqtt +""" + +from typing import List + +from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice +from bluetti_mqtt.core.devices.struct import DeviceStruct +from bluetti_mqtt.core.commands import ReadHoldingRegisters + +class EP600(BluettiDevice): + def __init__(self, address: str, sn: str): + self.struct = DeviceStruct() + + self.struct.add_uint_field('total_battery_percent', 102) + self.struct.add_swap_string_field('device_type', 110, 6) + self.struct.add_sn_field('serial_number', 116) + self.struct.add_decimal_field('power_generation', 154, 1) # Total power generated since last reset (kwh) + self.struct.add_swap_string_field('device_type', 1101, 6) + self.struct.add_sn_field('serial_number', 1107) + self.struct.add_decimal_field('power_generation', 1202, 1) # Total power generated since last reset (kwh) + self.struct.add_uint_field('battery_range_start', 2022) + self.struct.add_uint_field('battery_range_end', 2023) + self.struct.add_uint_field('max_ac_input_power', 2213) + self.struct.add_uint_field('max_ac_input_current', 2214) + self.struct.add_uint_field('max_ac_output_power', 2215) + self.struct.add_uint_field('max_ac_output_current', 2216) + self.struct.add_swap_string_field('battery_type', 6101, 6) + self.struct.add_sn_field('battery_serial_number', 6107) + self.struct.add_version_field('bcu_version', 6175) + self.struct.add_version_field('bmu_version', 6178) + self.struct.add_version_field('safety_module_version', 6181) + self.struct.add_version_field('high_voltage_module_version', 6184) + + # DC Solar Input (copied from PR https://github.com/warhammerkid/bluetti_mqtt/pull/87 by KM011092) + self.struct.add_uint_field('pv_input_power1', 1212) # MPP 1 in - value * 0.1 + #self.struct.add_uint_field('pv_input_voltage1', 1213) # MPP 1 in - value * 0.1 + #self.struct.add_uint_field('pv_input_current1', 1214) # MPP 1 in + self.struct.add_uint_field('pv_input_power2', 1220) # MPP 2 in - value * 0.1 + #self.struct.add_uint_field('pv_input_voltage2', 1221) # MPP 2 in - value * 0.1 + #self.struct.add_uint_field('pv_input_current2', 1222) # MPP 2 in + + # 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_phase2", 1236) + self.struct.add_uint_field("adl400_ac_input_power_phase3", 1244) + self.struct.add_decimal_field("adl400_ac_input_voltage_phase1", 1229, 1) + self.struct.add_decimal_field("adl400_ac_input_voltage_phase2", 1237, 1) + self.struct.add_decimal_field("adl400_ac_input_voltage_phase3", 1245, 1) + + # Grid Input + self.struct.add_decimal_field("grid_input_frequency", 1300, 1) + self.struct.add_uint_field("grid_input_power_phase1", 1313) + self.struct.add_uint_field("grid_input_power_phase2", 1319) + self.struct.add_uint_field("grid_input_power_phase3", 1325) + self.struct.add_decimal_field("grid_input_voltage_phase1", 1314, 1) + self.struct.add_decimal_field("grid_input_voltage_phase2", 1320, 1) + self.struct.add_decimal_field("grid_input_voltage_phase3", 1326, 1) + self.struct.add_decimal_field("grid_input_current_phase1", 1315, 1) + self.struct.add_decimal_field("grid_input_current_phase2", 1321, 1) + self.struct.add_decimal_field("grid_input_current_phase3", 1327, 1) + + # EP600 AC Output + self.struct.add_decimal_field("ac_output_frequency", 1500, 1) + self.struct.add_uint_field("ac_output_power_phase1", 1510) + self.struct.add_uint_field("ac_output_power_phase2", 1517) + self.struct.add_uint_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_phase2", 1518, 1) + self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1) + self.struct.add_decimal_field("ac_output_current_phase1", 1512, 1) + self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1) + self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1) + + super().__init__(address, 'EP600', sn) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(100, 62), + ReadHoldingRegisters(1228, 19), + ReadHoldingRegisters(1300, 28), + ReadHoldingRegisters(1500, 27), + ReadHoldingRegisters(2022, 6), + ReadHoldingRegisters(2213, 4), + ReadHoldingRegisters(1212, 11), + # Battery + ReadHoldingRegisters(6101, 7), + ReadHoldingRegisters(6175, 11), + ] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return [] + + @property + def logging_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(100, 62), + ReadHoldingRegisters(1100, 51), + ReadHoldingRegisters(1200, 90), + ReadHoldingRegisters(1300, 31), + ReadHoldingRegisters(1400, 48), + ReadHoldingRegisters(1500, 30), + ReadHoldingRegisters(2000, 89), + ReadHoldingRegisters(2200, 41), + ReadHoldingRegisters(2300, 36), + ReadHoldingRegisters(6000, 32), + ReadHoldingRegisters(6100, 100), + ReadHoldingRegisters(6300, 100), + ] diff --git a/custom_components/bluetti_bt/devices/ep700.py b/custom_components/bluetti_bt/devices/ep700.py new file mode 100644 index 0000000..b6ae3f0 --- /dev/null +++ b/custom_components/bluetti_bt/devices/ep700.py @@ -0,0 +1,67 @@ +"""EP700 Field definitions. + +Changes made here are only temporary and should also be +contributed to https://github.com/warhammerkid/bluetti_mqtt +""" + +from typing import List + +from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice +from bluetti_mqtt.core.devices.struct import DeviceStruct +from bluetti_mqtt.core.commands import ReadHoldingRegisters + +class EP700(BluettiDevice): + def __init__(self, address: str, sn: str): + self.struct = DeviceStruct() + + # TODO has to be tested + + self.struct.add_uint_field('total_battery_percent', 102) + self.struct.add_swap_string_field('device_type', 110, 6) + self.struct.add_sn_field('serial_number', 116) + self.struct.add_decimal_field('power_generation', 154, 1) + self.struct.add_swap_string_field('device_type', 1101, 6) + self.struct.add_sn_field('serial_number', 1107) + self.struct.add_decimal_field('power_generation', 1202, 1) + self.struct.add_uint_field('battery_range_start', 2022) + self.struct.add_uint_field('battery_range_end', 2023) + self.struct.add_uint_field('max_ac_input_power', 2213) + self.struct.add_uint_field('max_ac_input_current', 2214) + self.struct.add_uint_field('max_ac_output_power', 2215) + self.struct.add_uint_field('max_ac_output_current', 2216) + self.struct.add_swap_string_field('battery_type', 6101, 6) + self.struct.add_sn_field('battery_serial_number', 6107) + self.struct.add_version_field('bcu_version', 6175) + self.struct.add_version_field('bmu_version', 6178) + self.struct.add_version_field('safety_module_version', 6181) + self.struct.add_version_field('high_voltage_module_version', 6184) + + super().__init__(address, 'EP700', sn) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(100, 62), + ReadHoldingRegisters(2022, 2), + ] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return [] + + @property + def logging_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(100, 62), + ReadHoldingRegisters(1100, 51), + ReadHoldingRegisters(1200, 90), + ReadHoldingRegisters(1300, 31), + ReadHoldingRegisters(1400, 48), + ReadHoldingRegisters(1500, 30), + ReadHoldingRegisters(2000, 89), + ReadHoldingRegisters(2200, 41), + ReadHoldingRegisters(2300, 36), + ReadHoldingRegisters(6000, 32), + ReadHoldingRegisters(6100, 100), + ReadHoldingRegisters(6300, 100), + ] diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index da4e654..81ad886 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -8,7 +8,8 @@ { "local_name": "AC60*" }, { "local_name": "EB3A*" }, { "local_name": "EP500*" }, - { "local_name": "EP600*" } + { "local_name": "EP600*" }, + { "local_name": "EP700*" } ], "codeowners": ["@Patrick762"], "config_flow": true, @@ -18,5 +19,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": ["bluetti_mqtt==0.15.0"], - "version": "0.0.11" + "version": "0.0.12" } diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 428800e..5d16f15 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -22,7 +22,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) -from bluetti_mqtt.bluetooth import build_device from bluetti_mqtt.mqtt_client import ( NORMAL_DEVICE_FIELDS, DC_INPUT_FIELDS, @@ -30,9 +29,9 @@ from bluetti_mqtt.mqtt_client import ( battery_pack_fields, ) -from . import device_info as dev_info, get_unique_id +from . import device_info as dev_info, get_unique_id, build_device from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS -from .coordinator import PollingCoordinator, DummyDevice +from .coordinator import PollingCoordinator from .utils import unique_id_loggable _LOGGER = logging.getLogger(__name__) @@ -54,7 +53,6 @@ async def async_setup_entry( # Add sensors according to device_info bluetti_device = build_device(address, device_name) - bluetti_device = DummyDevice(bluetti_device) sensors_to_add = [] all_fields = NORMAL_DEVICE_FIELDS diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 62280e4..95fa189 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -23,7 +23,6 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) -from bluetti_mqtt.bluetooth import build_device from bluetti_mqtt.bluetooth.client import BluetoothClient from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice from bluetti_mqtt.mqtt_client import ( @@ -32,9 +31,9 @@ from bluetti_mqtt.mqtt_client import ( MqttFieldType, ) -from . import device_info as dev_info, get_unique_id +from . import device_info as dev_info, get_unique_id, build_device from .const import CONTROL_FIELDS, DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS -from .coordinator import PollingCoordinator, DummyDevice +from .coordinator import PollingCoordinator from .utils import mac_loggable, unique_id_loggable _LOGGER = logging.getLogger(__name__) @@ -56,7 +55,6 @@ async def async_setup_entry( # Add sensors according to device_info bluetti_device = build_device(address, device_name) - bluetti_device = DummyDevice(bluetti_device) sensors_to_add = [] all_fields = NORMAL_DEVICE_FIELDS From 9ef9d38a6eaf9eaf42de4fc2c0bb89fd3b593791 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 4 Feb 2024 12:05:53 +0100 Subject: [PATCH 04/82] renamed ep700 to ep760 --- README.md | 2 +- custom_components/bluetti_bt/__init__.py | 10 +++++----- custom_components/bluetti_bt/const.py | 2 +- .../bluetti_bt/devices/{ep700.py => ep760.py} | 6 +++--- custom_components/bluetti_bt/manifest.json | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) rename custom_components/bluetti_bt/devices/{ep700.py => ep760.py} (96%) diff --git a/README.md b/README.md index 28b3d6b..38e5292 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ After the installation, you can use this button to install the integration: - EP500 - EP500P - EP600 (tested) -- EP700 (beta, untested) +- EP760 (beta, untested) ### Available sensors: All sensors which are available in bluetti_mqtt (Based on [this file](https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/mqtt_client.py)). diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 5aeee6b..68e4d30 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -28,7 +28,7 @@ from .const import ( ) from .coordinator import PollingCoordinator from .devices.ep600 import EP600 -from .devices.ep700 import EP700 +from .devices.ep760 import EP760 PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -107,14 +107,14 @@ def get_type_by_bt_name(bt_name: str): dev_type = "EP500" elif bt_name.startswith("EP600"): dev_type = "EP600" - elif bt_name.startswith("EP700"): - dev_type = "EP700" + elif bt_name.startswith("EP760"): + dev_type = "EP760" return dev_type def build_device(address: str, name: str): match = DEVICE_NAME_RE.match(name) - if match[1] == 'EP700': - return EP700(address, match[2]) + if match[1] == 'EP760': + return EP760(address, match[2]) elif match[1] == 'EP600': return EP600(address, match[2]) else: diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index e25a033..545e80f 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -17,7 +17,7 @@ CONF_MAX_RETRIES = "max_retries" DATA_COORDINATOR = "coordinator" DATA_POLLING_RUNNING = "polling_running" -DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|EP500P|EP500|EP600|EP700|EB3A)(\d+)$') +DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|EP500P|EP500|EP600|EP760|EB3A)(\d+)$') CONTROL_FIELDS = [ "ac_output_on", diff --git a/custom_components/bluetti_bt/devices/ep700.py b/custom_components/bluetti_bt/devices/ep760.py similarity index 96% rename from custom_components/bluetti_bt/devices/ep700.py rename to custom_components/bluetti_bt/devices/ep760.py index b6ae3f0..6028f23 100644 --- a/custom_components/bluetti_bt/devices/ep700.py +++ b/custom_components/bluetti_bt/devices/ep760.py @@ -1,4 +1,4 @@ -"""EP700 Field definitions. +"""EP760 Field definitions. Changes made here are only temporary and should also be contributed to https://github.com/warhammerkid/bluetti_mqtt @@ -10,7 +10,7 @@ from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice from bluetti_mqtt.core.devices.struct import DeviceStruct from bluetti_mqtt.core.commands import ReadHoldingRegisters -class EP700(BluettiDevice): +class EP760(BluettiDevice): def __init__(self, address: str, sn: str): self.struct = DeviceStruct() @@ -36,7 +36,7 @@ class EP700(BluettiDevice): self.struct.add_version_field('safety_module_version', 6181) self.struct.add_version_field('high_voltage_module_version', 6184) - super().__init__(address, 'EP700', sn) + super().__init__(address, 'EP760', sn) @property def polling_commands(self) -> List[ReadHoldingRegisters]: diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 81ad886..4ad5e68 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -9,7 +9,7 @@ { "local_name": "EB3A*" }, { "local_name": "EP500*" }, { "local_name": "EP600*" }, - { "local_name": "EP700*" } + { "local_name": "EP760*" } ], "codeowners": ["@Patrick762"], "config_flow": true, @@ -19,5 +19,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": ["bluetti_mqtt==0.15.0"], - "version": "0.0.12" + "version": "0.0.13" } From ebbcea3c4c437b885ed0d9efdce8a2965b28c5fd Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Mon, 5 Feb 2024 19:10:11 +0100 Subject: [PATCH 05/82] fixed circular import --- custom_components/bluetti_bt/__init__.py | 14 -------------- custom_components/bluetti_bt/binary_sensor.py | 4 ++-- custom_components/bluetti_bt/coordinator.py | 3 +-- custom_components/bluetti_bt/manifest.json | 2 +- custom_components/bluetti_bt/sensor.py | 4 ++-- custom_components/bluetti_bt/switch.py | 4 ++-- custom_components/bluetti_bt/utils.py | 15 +++++++++++++++ 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 68e4d30..9ef157b 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -12,8 +12,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.exceptions import ConfigEntryNotReady -from bluetti_mqtt.bluetooth import build_device as orig_build_device - from .const import ( CONF_MAX_RETRIES, CONF_PERSISTENT_CONN, @@ -22,13 +20,10 @@ from .const import ( CONF_USE_CONTROLS, DATA_COORDINATOR, DATA_POLLING_RUNNING, - DEVICE_NAME_RE, DOMAIN, MANUFACTURER, ) from .coordinator import PollingCoordinator -from .devices.ep600 import EP600 -from .devices.ep760 import EP760 PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -110,12 +105,3 @@ def get_type_by_bt_name(bt_name: str): elif bt_name.startswith("EP760"): dev_type = "EP760" return dev_type - -def build_device(address: str, name: str): - match = DEVICE_NAME_RE.match(name) - if match[1] == 'EP760': - return EP760(address, match[2]) - elif match[1] == 'EP600': - return EP600(address, match[2]) - else: - return orig_build_device(address, name) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index fa02c25..a0716f2 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -24,10 +24,10 @@ from bluetti_mqtt.mqtt_client import ( MqttFieldType, ) -from . import device_info as dev_info, get_unique_id, build_device +from . import device_info as dev_info, get_unique_id from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator -from .utils import unique_id_loggable +from .utils import unique_id_loggable, build_device _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 304e1e7..0b91ff3 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -25,9 +25,8 @@ from bluetti_mqtt.bluetooth import ( from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice from bluetti_mqtt.core.commands import ReadHoldingRegisters -from . import build_device from .const import DATA_POLLING_RUNNING, DOMAIN -from .utils import mac_loggable +from .utils import mac_loggable, build_device _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 4ad5e68..4c32ba3 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": ["bluetti_mqtt==0.15.0"], - "version": "0.0.13" + "version": "0.0.14" } diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index fb2a17a..91e69c7 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -29,10 +29,10 @@ from bluetti_mqtt.mqtt_client import ( battery_pack_fields, ) -from . import device_info as dev_info, get_unique_id, build_device +from . import device_info as dev_info, get_unique_id from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator -from .utils import unique_id_loggable +from .utils import unique_id_loggable, build_device _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index b3a8e48..cca5a60 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -31,10 +31,10 @@ from bluetti_mqtt.mqtt_client import ( MqttFieldType, ) -from . import device_info as dev_info, get_unique_id, build_device +from . import device_info as dev_info, get_unique_id from .const import CONTROL_FIELDS, DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator -from .utils import mac_loggable, unique_id_loggable +from .utils import mac_loggable, unique_id_loggable, build_device _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/utils.py b/custom_components/bluetti_bt/utils.py index fce973c..a42f928 100644 --- a/custom_components/bluetti_bt/utils.py +++ b/custom_components/bluetti_bt/utils.py @@ -1,5 +1,11 @@ """Tools.""" +from bluetti_mqtt.bluetooth import build_device as orig_build_device + +from .const import DEVICE_NAME_RE +from .devices.ep600 import EP600 +from .devices.ep760 import EP760 + def mac_loggable(mac: str) -> str: """Remove parts of the mac address for logging.""" splitted = mac.split(":") @@ -10,3 +16,12 @@ def unique_id_loggable(unique_id: str) -> str: splitted = unique_id.split("_", maxsplit=1) serial = splitted[0][:6] return serial + "XXXXXXXXXX" + splitted[1] + +def build_device(address: str, name: str): + match = DEVICE_NAME_RE.match(name) + if match[1] == 'EP760': + return EP760(address, match[2]) + elif match[1] == 'EP600': + return EP600(address, match[2]) + else: + return orig_build_device(address, name) From 3e4b8601e88a88f590029322923ad548918328c1 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 10 Feb 2024 13:19:16 +0100 Subject: [PATCH 06/82] added dropdown to select device model --- custom_components/bluetti_bt/config_flow.py | 17 +++++++++++++++-- custom_components/bluetti_bt/const.py | 11 +++++++++++ custom_components/bluetti_bt/manifest.json | 5 +++-- .../bluetti_bt/translations/de.json | 10 ++++++++++ .../bluetti_bt/translations/en.json | 10 ++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 61d78c9..cc300e4 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -24,6 +24,7 @@ from .const import ( CONF_POLLING_TIMEOUT, CONF_USE_CONTROLS, DOMAIN, + SUPPORTED_MODELS, ) _LOGGER = logging.getLogger(__name__) @@ -58,12 +59,18 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() dev_type = get_type_by_bt_name(discovery_info.name) + name = re.sub("[^A-Z0-9]+", "", discovery_info.name) + + if dev_type == "Unknown": + dev_type = user_input[CONF_TYPE] + name = re.sub("[^A-Z0-9]+", "", discovery_info.name).replace("PBOX", dev_type) + return self.async_create_entry( - title=discovery_info.name, + title=name, data={ CONF_ADDRESS: discovery_info.address, CONF_TYPE: dev_type, - CONF_NAME: re.sub("[^A-Z0-9]+", "", discovery_info.name), + CONF_NAME: name, }, ) @@ -88,6 +95,12 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for service_info in self._discovered_devices.values() } ), + vol.Optional(CONF_TYPE): vol.In( + { + model: model + for model in SUPPORTED_MODELS + } + ), } ) return self.async_show_form( diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 545e80f..7015358 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -17,6 +17,17 @@ CONF_MAX_RETRIES = "max_retries" DATA_COORDINATOR = "coordinator" DATA_POLLING_RUNNING = "polling_running" +SUPPORTED_MODELS = [ + "AC60", + "AC200M", + "AC300", + "AC500", + "EB3A", + "EP500", + "EP500P", + "EP600", + "EP760", +] DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|EP500P|EP500|EP600|EP760|EB3A)(\d+)$') CONTROL_FIELDS = [ diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 4c32ba3..56fe455 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -9,7 +9,8 @@ { "local_name": "EB3A*" }, { "local_name": "EP500*" }, { "local_name": "EP600*" }, - { "local_name": "EP760*" } + { "local_name": "EP760*" }, + { "local_name": "PBOX*" } ], "codeowners": ["@Patrick762"], "config_flow": true, @@ -19,5 +20,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": ["bluetti_mqtt==0.15.0"], - "version": "0.0.14" + "version": "0.0.15" } diff --git a/custom_components/bluetti_bt/translations/de.json b/custom_components/bluetti_bt/translations/de.json index 7093834..9451cab 100644 --- a/custom_components/bluetti_bt/translations/de.json +++ b/custom_components/bluetti_bt/translations/de.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "address": "Gerät", + "type": "Modell (erforderlich bei PBOX Geräten)" + } + } + } + }, "options": { "step": { "init": { diff --git a/custom_components/bluetti_bt/translations/en.json b/custom_components/bluetti_bt/translations/en.json index 1229863..c5b4610 100644 --- a/custom_components/bluetti_bt/translations/en.json +++ b/custom_components/bluetti_bt/translations/en.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "address": "Device", + "type": "Model (required for PBOX devices)" + } + } + } + }, "options": { "step": { "init": { From ba6611b084e6c772d31b4b5cf02bbc0c64997792 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 10 Feb 2024 13:21:49 +0100 Subject: [PATCH 07/82] removed duplicate re.sub() --- custom_components/bluetti_bt/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index cc300e4..9ebd5b8 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -63,7 +63,7 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if dev_type == "Unknown": dev_type = user_input[CONF_TYPE] - name = re.sub("[^A-Z0-9]+", "", discovery_info.name).replace("PBOX", dev_type) + name = name.replace("PBOX", dev_type) return self.async_create_entry( title=name, From ad536235bb478043c3e5af51b9bb47e2ea6146e6 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 10 Feb 2024 13:59:01 +0100 Subject: [PATCH 08/82] removed device name from entity name --- custom_components/bluetti_bt/binary_sensor.py | 2 +- custom_components/bluetti_bt/sensor.py | 2 +- custom_components/bluetti_bt/switch.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index a0716f2..b8e5598 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -95,7 +95,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): self._response_key = response_key self._attr_device_info = device_info - self._attr_name = e_name + self._attr_name = name self._attr_available = False self._attr_unique_id = get_unique_id(e_name) self._attr_entity_category = category diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 91e69c7..09a81a7 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -126,7 +126,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): self._response_key = response_key self._attr_device_info = device_info - self._attr_name = e_name + self._attr_name = name self._attr_available = False self._attr_unique_id = get_unique_id(e_name) self._attr_native_unit_of_measurement = unit_of_measurement diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index cca5a60..02fd57f 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -107,7 +107,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): self._attr_device_info = device_info self._attr_has_entity_name = True - self._attr_name = e_name + self._attr_name = name self._attr_available = False self._attr_unique_id = get_unique_id(e_name) self._attr_entity_category = category From 58f2ec50c4518702809936f35b00c07970a95db7 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 10 Feb 2024 14:15:52 +0100 Subject: [PATCH 09/82] added sleep() between pack pollings --- custom_components/bluetti_bt/coordinator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 0b91ff3..03b1a2b 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -136,6 +136,9 @@ class PollingCoordinator(DataUpdateCoordinator): self.bluetti_device.build_setter_command('pack_num', pack) ) + # We need to wait after switching packs for the data to be available + await asyncio.sleep(5) + for command in self.bluetti_device.pack_polling_commands: # Request & parse result for each pack try: From 0943ecf187af99ff0995a0d0e42c983c6d8140ac Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 10 Feb 2024 14:20:37 +0100 Subject: [PATCH 10/82] fix manifest version number --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 56fe455..c79a35a 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -20,5 +20,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": ["bluetti_mqtt==0.15.0"], - "version": "0.0.15" + "version": "0.0.16" } From f6f19b27e11aaca89b4663d4f511ccdbc4017d86 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 16 Feb 2024 21:07:06 +0100 Subject: [PATCH 11/82] added local bluetti lib --- .../bluetti_bt/bluetti_bt_lib/__init__.py | 3 + .../base_devices/BluettiDevice.py | 63 ++++++ .../bluetti_bt/bluetti_bt_lib/const.py | 6 + .../bluetti_bt/bluetti_bt_lib/exceptions.py | 14 ++ .../bluetti_bt_lib/utils/commands.py | 111 +++++++++++ .../bluetti_bt/bluetti_bt_lib/utils/struct.py | 179 ++++++++++++++++++ custom_components/bluetti_bt/coordinator.py | 21 +- 7 files changed, 384 insertions(+), 13 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/__init__.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/const.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/__init__.py b/custom_components/bluetti_bt/bluetti_bt_lib/__init__.py new file mode 100644 index 0000000..36a27e2 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/__init__.py @@ -0,0 +1,3 @@ +from .const import * +from .exceptions import * +from .utils.commands import ReadHoldingRegisters diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py new file mode 100644 index 0000000..8ce7432 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py @@ -0,0 +1,63 @@ +"""Bluetti device.""" + +# Reduced copy of https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/core/devices/bluetti_device.py + +from typing import Any, List + +from ..utils.commands import ReadHoldingRegisters, WriteSingleRegister +from ..utils.struct import BoolField, DeviceStruct, EnumField + + +class BluettiDevice: + struct: DeviceStruct + + def __init__(self, address: str, type: str, sn: str): + self.address = address + self.type = type + self.sn = sn + + def parse(self, address: int, data: bytes) -> dict: + return self.struct.parse(address, data) + + @property + def pack_num_max(self): + """ + A given device has a maximum number of battery packs, including the + internal battery if it has one. We can provide this information statically + so it's not necessary to poll the device. + """ + return 1 + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + """A given device has an optimal set of commands for polling""" + raise NotImplementedError + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + """A given device may have a set of commands for polling pack data""" + return [] + + @property + def writable_ranges(self) -> List[range]: + """The address ranges that are writable""" + return [] + + def has_field(self, field: str): + return any(f.name == field for f in self.struct.fields) + + def has_field_setter(self, field: str): + matches = [f for f in self.struct.fields if f.name == field] + return any(any(f.address in r for r in self.writable_ranges) for f in matches) + + def build_setter_command(self, field: str, value: Any): + matches = [f for f in self.struct.fields if f.name == field] + device_field = next(f for f in matches if any(f.address in r for r in self.writable_ranges)) + + # Convert value to an integer + if isinstance(device_field, EnumField): + value = device_field.enum[value].value + elif isinstance(device_field, BoolField): + value = 1 if value else 0 + + return WriteSingleRegister(device_field.address, value) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/const.py b/custom_components/bluetti_bt/bluetti_bt_lib/const.py new file mode 100644 index 0000000..7bc8f82 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/const.py @@ -0,0 +1,6 @@ +"""Const definitions.""" + +RESPONSE_TIMEOUT = 5 +WRITE_UUID = '0000ff02-0000-1000-8000-00805f9b34fb' +NOTIFY_UUID = '0000ff01-0000-1000-8000-00805f9b34fb' +DEVICE_NAME_UUID = '00002a00-0000-1000-8000-00805f9b34fb' diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py b/custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py new file mode 100644 index 0000000..6e708b9 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py @@ -0,0 +1,14 @@ +"""Exceptions.""" + +# Modified copy of https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/bluetooth/exc.py + +class ParseError(Exception): + pass + + +class ModbusError(Exception): + pass + + +class BadConnectionError(Exception): + pass diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py new file mode 100644 index 0000000..87cad7b --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py @@ -0,0 +1,111 @@ +"""Device commands.""" + +# Copy of https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/core/commands.py + +import struct +import crcmod.predefined + +modbus_crc = crcmod.predefined.mkCrcFun('modbus') + + +class DeviceCommand: + def __init__(self, function_code: int, data: bytes): + self.function_code = function_code + + self.cmd = bytearray(len(data) + 4) + self.cmd[0] = 1 # MODBUS address + self.cmd[1] = function_code + self.cmd[2:-2] = data + struct.pack_into(' int: + """Returns the expected response size in bytes""" + pass + + def __iter__(self): + """Provide an iter implemention so that bytes(cmd) works""" + return iter(self.cmd) + + def is_exception_response(self, response: bytes): + """Checks the response code to see if it's a MODBUS exception""" + if len(response) < 2: + return False + else: + return response[1] == self.function_code + 0x80 + + def is_valid_response(self, response: bytes): + """Validates that the reponse is complete and uncorrupted""" + if len(response) < 3: + return False + + crc = modbus_crc(response[0:-2]) + crc_bytes = crc.to_bytes(2, byteorder='little') + return response[-2:] == crc_bytes + + def parse_response(self, response: bytes): + """Returns the raw body of the response""" + return response + + +class ReadHoldingRegisters(DeviceCommand): + def __init__(self, starting_address: int, quantity: int): + self.starting_address = starting_address + self.quantity = quantity + + super().__init__(3, struct.pack('!HH', starting_address, quantity)) + + def response_size(self): + # 3 byte header + # each returned field is actually 2 bytes (16-bit word) + # 2 byte crc + return 2 * self.quantity + 5 + + def parse_response(self, response: bytes): + return bytes(response[3:-2]) + + def __repr__(self): + return ( + f'ReadHoldingRegisters(starting_address={self.starting_address}, quantity={self.quantity})' + ) + + +class WriteSingleRegister(DeviceCommand): + def __init__(self, address: int, value: int): + self.address = address + self.value = value + + super().__init__(6, struct.pack('!HH', address, value)) + + def response_size(self): + return 8 + + def parse_response(self, response: bytes): + return bytes(response[4:6]) + + def __repr__(self): + return ( + f'WriteSingleRegister(address={self.address}, value={self.value:#04x})' + ) + + +class WriteMultipleRegisters(DeviceCommand): + def __init__(self, starting_address: int, data: bytes): + if len(data) % 2 != 0: + raise ValueError('data size must be multiple of 2') + + self.starting_address = starting_address + self.data = data + + body = bytearray(len(data) + 5) + half_len = len(data) >> 1 + struct.pack_into('!HHB', body, 0, starting_address, half_len, len(data)) + body[5:] = data + super().__init__(16, body) + + def response_size(self): + return 8 + + def __repr__(self): + return ( + f'WriteMultipleRegisters(starting_address={self.starting_address}, data={self.data})' + ) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py new file mode 100644 index 0000000..9e9f27b --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -0,0 +1,179 @@ +"""Struct definitions.""" + +# Copy of https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/core/devices/struct.py + +from decimal import Decimal +from enum import Enum +import struct +from typing import Any, List, Optional, Tuple, Type + + +def swap_bytes(data: bytes): + """Swaps the place of every other byte, returning a new byte array""" + arr = bytearray(data) + for i in range(0, len(arr) - 1, 2): + arr[i], arr[i + 1] = arr[i + 1], arr[i] + return arr + + +class DeviceField: + def __init__(self, name: str, address: int, size: int): + self.name = name + self.address = address + self.size = size + + def parse(self, data: bytes) -> Any: + raise NotImplementedError + + def in_range(self, val: Any) -> bool: + return True + + +class UintField(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): + def __init__(self, name: str, address: int): + super().__init__(name, address, 1) + + def parse(self, data: bytes) -> bool: + return struct.unpack('!H', data)[0] == 1 + + +class EnumField(DeviceField): + def __init__(self, name: str, address: int, enum: Type[Enum]): + self.enum = enum + super().__init__(name, address, 1) + + def parse(self, data: bytes) -> Any: + val = struct.unpack('!H', data)[0] + return self.enum(val) + + +class DecimalField(DeviceField): + def __init__(self, name: str, address: int, scale: int, range: Optional[Tuple[int, int]]): + self.scale = scale + self.range = range + super().__init__(name, address, 1) + + def parse(self, data: bytes) -> Decimal: + val = Decimal(struct.unpack('!H', data)[0]) + return val / 10 ** self.scale + + def in_range(self, val: Decimal) -> bool: + if self.range is None: + return True + else: + return val >= self.range[0] and val <= self.range[1] + + +class DecimalArrayField(DeviceField): + def __init__(self, name: str, address: int, size: int, scale: int): + self.scale = scale + super().__init__(name, address, size) + + def parse(self, data: bytes) -> Decimal: + values = list(struct.unpack(f'!{self.size}H', data)) + return [Decimal(v) / 10 ** self.scale for v in values] + + +class StringField(DeviceField): + """Fixed-width null-terminated string field""" + def parse(self, data: bytes) -> str: + return data.rstrip(b'\0').decode('ascii') + + +class SwapStringField(DeviceField): + """Fixed-width null-terminated string field""" + def parse(self, data: bytes) -> str: + return swap_bytes(data).rstrip(b'\0').decode('ascii') + + +class VersionField(DeviceField): + def __init__(self, name: str, address: int): + super().__init__(name, address, 2) + + def parse(self, data: bytes) -> int: + values = struct.unpack('!2H', data) + return Decimal(values[0] + (values[1] << 16)) / 100 + + +class SerialNumberField(DeviceField): + def __init__(self, name: str, address: int): + super().__init__(name, address, 4) + + def parse(self, data: bytes) -> int: + values = struct.unpack('!4H', data) + return values[0] + (values[1] << 16) + (values[2] << 32) + (values[3] << 48) + + +class DeviceStruct: + fields: List[DeviceField] + + def __init__(self): + self.fields = [] + + def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None): + self.fields.append(UintField(name, address, range)) + + def add_bool_field(self, name: str, address: int): + self.fields.append(BoolField(name, address)) + + def add_enum_field(self, name: str, address: int, enum: Type[Enum]): + self.fields.append(EnumField(name, address, enum)) + + def add_decimal_field(self, name: str, address: int, scale: int, range: Tuple[int, int] = None): + self.fields.append(DecimalField(name, address, scale, range)) + + def add_decimal_array_field(self, name: str, address: int, size: int, scale: int): + self.fields.append(DecimalArrayField(name, address, size, scale)) + + def add_string_field(self, name: str, address: int, size: int): + self.fields.append(StringField(name, address, size)) + + def add_swap_string_field(self, name: str, address: int, size: int): + self.fields.append(SwapStringField(name, address, size)) + + def add_version_field(self, name: str, address: int): + self.fields.append(VersionField(name, address)) + + def add_sn_field(self, name: str, address: int): + self.fields.append(SerialNumberField(name, address)) + + def parse(self, starting_address: int, data: bytes) -> dict: + # Offsets and size are counted in 2 byte chunks, so for the range we + # need to divide the byte size by 2 + data_size = int(len(data) / 2) + + # Filter out fields not in range + r = range(starting_address, starting_address + data_size) + fields = [f for f in self.fields + if f.address in r and f.address + f.size - 1 in r] + + # Parse fields + parsed = {} + for f in fields: + data_start = 2 * (f.address - starting_address) + field_data = data[data_start:data_start + 2 * f.size] + val = f.parse(field_data) + + # Skip if the value is "out-of-range" - sometimes the sensors + # report weird values + if not f.in_range(val): + continue + + parsed[f.name] = val + + return parsed diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 03b1a2b..40329d5 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -1,7 +1,7 @@ """Coordinator for Bluetti integration.""" from __future__ import annotations -from typing import List, cast +from typing import cast import asyncio from datetime import timedelta @@ -16,14 +16,9 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from bluetti_mqtt.bluetooth.client import BluetoothClient -from bluetti_mqtt.bluetooth import ( - BadConnectionError, - ModbusError, - ParseError, -) -from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice -from bluetti_mqtt.core.commands import ReadHoldingRegisters +from .bluetti_bt_lib.const import NOTIFY_UUID, RESPONSE_TIMEOUT, WRITE_UUID +from .bluetti_bt_lib.exceptions import BadConnectionError, ModbusError, ParseError +from .bluetti_bt_lib.utils.commands import ReadHoldingRegisters from .const import DATA_POLLING_RUNNING, DOMAIN from .utils import mac_loggable, build_device @@ -108,7 +103,7 @@ class PollingCoordinator(DataUpdateCoordinator): # Attach notifier if needed if not self.has_notifier: await self.client.start_notify( - BluetoothClient.NOTIFY_UUID, self._notification_handler + NOTIFY_UUID, self._notification_handler ) self.has_notifier = True @@ -171,7 +166,7 @@ class PollingCoordinator(DataUpdateCoordinator): # Disconnect if connection not persistant if not self.persistent_conn: if self.has_notifier: - await self.client.stop_notify(BluetoothClient.NOTIFY_UUID) + await self.client.stop_notify(NOTIFY_UUID) self.has_notifier = False await self.client.disconnect() @@ -191,12 +186,12 @@ class PollingCoordinator(DataUpdateCoordinator): # Make request self.logger.debug("Requesting %s", command) await self.client.write_gatt_char( - BluetoothClient.WRITE_UUID, bytes(command) + WRITE_UUID, bytes(command) ) # Wait for response res = await asyncio.wait_for( - self.notify_future, timeout=BluetoothClient.RESPONSE_TIMEOUT + self.notify_future, timeout=RESPONSE_TIMEOUT ) # Process data From 4231b34bb8036573093304a3292085ac752f588a Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 16 Feb 2024 21:30:29 +0100 Subject: [PATCH 12/82] added basic v1 and v2 protocol devices --- .../base_devices/ProtocolV1Device.py | 48 +++++++++++++ .../base_devices/ProtocolV2Device.py | 49 +++++++++++++ .../bluetti_bt_lib/field_attributes.py | 72 +++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py new file mode 100644 index 0000000..d70d667 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -0,0 +1,48 @@ +"""Base device definition for V1 Protocol devices.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..utils.struct import DeviceStruct +from .BluettiDevice import BluettiDevice + + +class ProtocolV1Device(BluettiDevice): + def __init__(self, address: str, type: str, sn: str): + self.struct = DeviceStruct() + + # Device info + self.struct.add_string_field('device_type', 10, 6) + self.struct.add_sn_field('serial_number', 17) + + # Power IO + self.struct.add_uint_field('dc_input_power', 36) + self.struct.add_uint_field('ac_input_power', 37) + self.struct.add_uint_field('ac_output_power', 38) + self.struct.add_uint_field('dc_output_power', 39) + + # Battery + self.struct.add_uint_field('total_battery_percent', 43) + + # Output state + self.struct.add_bool_field('ac_output_on', 48) + self.struct.add_bool_field('dc_output_on', 49) + + # Output controls + self.struct.add_bool_field('ac_output_on_switch', 3007) + self.struct.add_bool_field('dc_output_on_switch', 3008) + + super().__init__(address, type, sn) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(10, 10), + ReadHoldingRegisters(36, 4), + ReadHoldingRegisters(43, 1), + ReadHoldingRegisters(48, 2), + ] + + @property + def writable_ranges(self) -> List[range]: + return [range(3007, 3008)] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py new file mode 100644 index 0000000..a3670ae --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py @@ -0,0 +1,49 @@ +"""Base device definition for V2 Protocol devices.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..utils.struct import DeviceStruct +from .BluettiDevice import BluettiDevice + + +class ProtocolV2Device(BluettiDevice): + def __init__(self, address: str, type: str, sn: str): + self.struct = DeviceStruct() + + # Device info + self.struct.add_string_field('device_type', 110, 6) + self.struct.add_sn_field('serial_number', 116) + + # Power IO + #self.struct.add_uint_field('dc_input_power', ?) + #self.struct.add_uint_field('ac_input_power', ?) + #self.struct.add_uint_field('ac_output_power', ?) + #self.struct.add_uint_field('dc_output_power', ?) + + # Battery + self.struct.add_uint_field('total_battery_percent', 102) + + # Output state + #self.struct.add_bool_field('ac_output_on', ?) + #self.struct.add_bool_field('dc_output_on', ?) + + # Output controls + #self.struct.add_bool_field('ac_output_on_switch', ?) + #self.struct.add_bool_field('dc_output_on_switch', ?) + + # Add battery fields here? + + super().__init__(address, type, sn) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(110, 6), + ReadHoldingRegisters(116, 4), + ReadHoldingRegisters(102, 1), + ] + + @property + def writable_ranges(self) -> List[range]: + return [] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py new file mode 100644 index 0000000..6d4ce70 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -0,0 +1,72 @@ +"""Field attributes for device fields.""" + +from dataclasses import dataclass +from enum import Enum, auto, unique + + +@unique +class FieldType(Enum): + NUMERIC = auto() + BOOL = auto() + ENUM = auto() + BUTTON = auto() + + +@dataclass(frozen=True) +class FieldAttributes: + type: FieldType + setter: bool = False + name: str + unit_of_measurement: str|None = None + device_class: str|None = None + state_class: str|None = None + +@dataclass(frozen=True) +class PowerFieldAttributes(FieldAttributes): + type = FieldType.NUMERIC + name: str + unit_of_measurement = 'W' + device_class = 'power' + state_class = 'measurement' + +@dataclass(frozen=True) +class OutletFieldAttributes(FieldAttributes): + type = FieldType.BOOL + name: str + device_class = 'outlet' + +FIELD_ATTRIBUTES = { + 'dc_input_power': PowerFieldAttributes( + name = 'DC Input Power', + ), + 'ac_input_power': PowerFieldAttributes( + name = 'AC Input Power', + ), + 'ac_output_power': PowerFieldAttributes( + name = 'AC Output Power', + ), + 'dc_output_power': PowerFieldAttributes( + name = 'DC Output Power', + ), + 'total_battery_percent': FieldAttributes( + type = FieldType.NUMERIC, + name = 'Total Battery Percent', + unit_of_measurement = '%', + device_class ='battery', + state_class ='measurement', + ), + 'ac_output_on': OutletFieldAttributes( + name = 'AC Output', + ), + 'dc_output_on': OutletFieldAttributes( + name = 'DC Output', + ), + 'ac_output_on_switch': OutletFieldAttributes( + name = 'AC Output', + setter = True, + ), + 'dc_output_on_switch': OutletFieldAttributes( + name = 'DC Output', + setter = True, + ), +} From cac64b40bdc7765e40b807aa4a3b193dd49d5755 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 17:25:57 +0100 Subject: [PATCH 13/82] added power_generation to base devices --- .../bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py | 3 +++ .../bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index d70d667..d7e3670 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -21,6 +21,9 @@ class ProtocolV1Device(BluettiDevice): self.struct.add_uint_field('ac_output_power', 38) self.struct.add_uint_field('dc_output_power', 39) + # Power IO statistics + self.struct.add_decimal_field('power_generation', 41, 1) # Total power generated since last reset (kwh) + # Battery self.struct.add_uint_field('total_battery_percent', 43) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py index a3670ae..a6a87ca 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py @@ -21,6 +21,9 @@ class ProtocolV2Device(BluettiDevice): #self.struct.add_uint_field('ac_output_power', ?) #self.struct.add_uint_field('dc_output_power', ?) + # Power IO statistics + self.struct.add_decimal_field('power_generation', 154, 1) # Total power generated since last reset (kwh) + # Battery self.struct.add_uint_field('total_battery_percent', 102) From 01cb7f35a5d39a38d2f7723f0fc39b8cc0437562 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 17:52:08 +0100 Subject: [PATCH 14/82] added pack fields to base devices --- .../base_devices/ProtocolV1Device.py | 11 ++++++ .../base_devices/ProtocolV2Device.py | 8 +++++ .../bluetti_bt_lib/field_attributes.py | 36 +++++++++++++++++-- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index d7e3670..82cdd44 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -27,6 +27,13 @@ class ProtocolV1Device(BluettiDevice): # Battery self.struct.add_uint_field('total_battery_percent', 43) + # Battery packs + self.struct.add_uint_field('pack_num_max', 91) # internal + self.struct.add_decimal_field('total_battery_voltage', 92, 1) + self.struct.add_uint_field('pack_num', 96) # internal + self.struct.add_decimal_field('pack_voltage', 98, 2) # Full pack voltage + self.struct.add_uint_field('pack_battery_percent', 99) + # Output state self.struct.add_bool_field('ac_output_on', 48) self.struct.add_bool_field('dc_output_on', 49) @@ -42,7 +49,11 @@ class ProtocolV1Device(BluettiDevice): return [ ReadHoldingRegisters(10, 10), ReadHoldingRegisters(36, 4), + ReadHoldingRegisters(41, 1), ReadHoldingRegisters(43, 1), + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), ReadHoldingRegisters(48, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py index a6a87ca..ff94034 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py @@ -27,6 +27,13 @@ class ProtocolV2Device(BluettiDevice): # Battery self.struct.add_uint_field('total_battery_percent', 102) + # Battery packs + #self.struct.add_uint_field('pack_num_max', ?) # internal + #self.struct.add_decimal_field('total_battery_voltage', ?, 1) + #self.struct.add_uint_field('pack_num', ?) # internal + #self.struct.add_decimal_field('pack_voltage', ?, 2) # Full pack voltage + #self.struct.add_uint_field('pack_battery_percent', ?) + # Output state #self.struct.add_bool_field('ac_output_on', ?) #self.struct.add_bool_field('dc_output_on', ?) @@ -44,6 +51,7 @@ class ProtocolV2Device(BluettiDevice): return [ ReadHoldingRegisters(110, 6), ReadHoldingRegisters(116, 4), + ReadHoldingRegisters(154, 1), ReadHoldingRegisters(102, 1), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 6d4ce70..577ab94 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -24,18 +24,24 @@ class FieldAttributes: @dataclass(frozen=True) class PowerFieldAttributes(FieldAttributes): type = FieldType.NUMERIC - name: str unit_of_measurement = 'W' device_class = 'power' state_class = 'measurement' +@dataclass +class VoltageFieldAttributes(FieldAttributes): + type = FieldType.NUMERIC + unit_of_measurement = 'V' + device_class = 'voltage' + state_class = 'measurement' + @dataclass(frozen=True) class OutletFieldAttributes(FieldAttributes): type = FieldType.BOOL - name: str device_class = 'outlet' FIELD_ATTRIBUTES = { + # Base device fields 'dc_input_power': PowerFieldAttributes( name = 'DC Input Power', ), @@ -48,6 +54,13 @@ FIELD_ATTRIBUTES = { 'dc_output_power': PowerFieldAttributes( name = 'DC Output Power', ), + 'power_generation': FieldAttributes( + type = FieldType.NUMERIC, + name = 'Total Power Generation', + unit_of_measurement = 'kWh', + device_class = 'energy', + state_class = 'total_increasing', + ), 'total_battery_percent': FieldAttributes( type = FieldType.NUMERIC, name = 'Total Battery Percent', @@ -55,6 +68,9 @@ FIELD_ATTRIBUTES = { device_class ='battery', state_class ='measurement', ), + 'total_battery_voltage': VoltageFieldAttributes( + name = 'Total Battery Voltage', + ), 'ac_output_on': OutletFieldAttributes( name = 'AC Output', ), @@ -69,4 +85,20 @@ FIELD_ATTRIBUTES = { name = 'DC Output', setter = True, ), + + # Device specific fields } + +def PACK_FIELD_ATTRIBUTES(pack: int): + return { + 'pack_voltage': VoltageFieldAttributes( + name = f'Battery Pack {pack} Voltage', + ), + 'pack_battery_percent': FieldAttributes( + type = FieldType, + name = f'Battery Pack {pack} Percent', + unit_of_measurement = '%', + device_class ='battery', + state_class ='measurement', + ), + } From f7b4d8dc3719ef13b91bd0d53cbbd92ab5d4f5c0 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 17:53:34 +0100 Subject: [PATCH 15/82] added ac60 --- .../bluetti_bt/bluetti_bt_lib/devices/ac60.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ac60.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac60.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac60.py new file mode 100644 index 0000000..de8a612 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac60.py @@ -0,0 +1,8 @@ +"""AC60 fields.""" + +from ..base_devices.ProtocolV2Device import ProtocolV2Device + + +class AC60(ProtocolV2Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "AC60", sn) From 037ce83891c7e2f072dacf21db3ef322c9810c6e Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 18:00:59 +0100 Subject: [PATCH 16/82] added pack selector --- .../bluetti_bt_lib/base_devices/ProtocolV1Device.py | 5 ++++- .../bluetti_bt_lib/base_devices/ProtocolV2Device.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index 82cdd44..05d2750 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -38,6 +38,9 @@ class ProtocolV1Device(BluettiDevice): self.struct.add_bool_field('ac_output_on', 48) self.struct.add_bool_field('dc_output_on', 49) + # Pack selector + self.struct.add_uint_field('pack_num', 3006) # internal + # Output controls self.struct.add_bool_field('ac_output_on_switch', 3007) self.struct.add_bool_field('dc_output_on_switch', 3008) @@ -59,4 +62,4 @@ class ProtocolV1Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: - return [range(3007, 3008)] + return [range(3006, 3008)] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py index ff94034..fa4f50c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py @@ -38,6 +38,9 @@ class ProtocolV2Device(BluettiDevice): #self.struct.add_bool_field('ac_output_on', ?) #self.struct.add_bool_field('dc_output_on', ?) + # Pack selector + #self.struct.add_uint_field('pack_num', ?) # internal + # Output controls #self.struct.add_bool_field('ac_output_on_switch', ?) #self.struct.add_bool_field('dc_output_on_switch', ?) From 4f92e9147e4e973729e8f26bf93fcd50e45be62c Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 18:07:51 +0100 Subject: [PATCH 17/82] added pack polling registers to base devices --- .../bluetti_bt_lib/base_devices/ProtocolV1Device.py | 8 ++++++++ .../bluetti_bt_lib/base_devices/ProtocolV2Device.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index 05d2750..ba6ba2d 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -63,3 +63,11 @@ class ProtocolV1Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: return [range(3006, 3008)] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py index fa4f50c..21abd98 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py @@ -61,3 +61,7 @@ class ProtocolV2Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: return [] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return [] From 5fcd57b517288771937d5fd1a7f033b618492291 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 18:08:05 +0100 Subject: [PATCH 18/82] added enums --- .../bluetti_bt/bluetti_bt_lib/field_enums.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py new file mode 100644 index 0000000..b1aecfc --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py @@ -0,0 +1,20 @@ +"""Enums for fields.""" + +from enum import Enum, unique + + +@unique +class OutputMode(Enum): + STOP = 0 + INVERTER_OUTPUT = 1 + BYPASS_OUTPUT_C = 2 + BYPASS_OUTPUT_D = 3 + LOAD_MATCHING = 4 + + +@unique +class AutoSleepMode(Enum): + THIRTY_SECONDS = 2 + ONE_MINUTE = 3 + FIVE_MINUTES = 4 + NEVER = 5 From d9d4860d4411ced7b52236470dcead40d3d9447e Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 18:22:41 +0100 Subject: [PATCH 19/82] added ac200m --- .../bluetti_bt_lib/devices/ac200m.py | 48 +++++++++++++++++ .../bluetti_bt_lib/field_attributes.py | 53 ++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py new file mode 100644 index 0000000..b2df3ee --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -0,0 +1,48 @@ +"""AC200M fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..field_enums import AutoSleepMode, OutputMode +from ..base_devices.ProtocolV1Device import ProtocolV1Device + + +class AC200M(ProtocolV1Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "AC200M", sn) + + # Details + self.struct.add_enum_field('ac_output_mode', 70, OutputMode) + self.struct.add_uint_field('internal_ac_voltage', 71) + self.struct.add_decimal_field('internal_current_one', 72, 1) + self.struct.add_uint_field('internal_power_one', 73) + self.struct.add_decimal_field('internal_ac_frequency', 74, 1) + self.struct.add_uint_field('internal_dc_input_voltage', 86) + self.struct.add_decimal_field('internal_dc_input_power', 87, 1) + self.struct.add_decimal_field('internal_dc_input_current', 88, 2) + + # Battery packs + self.struct.add_decimal_array_field('cell_voltages', 105, 16, 2) # internal + + # Controls + self.struct.add_bool_field('power_off', 3060) + self.struct.add_enum_field('auto_sleep_mode', 3061, AutoSleepMode) + + @property + def pack_num_max(self): + return 3 + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(70, 4), + ReadHoldingRegisters(86, 3), + ] + + @property + def writable_ranges(self) -> List[range]: + return super().writable_ranges + [range(3060, 3061)] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return super().pack_polling_commands + [ReadHoldingRegisters(105, 16)] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 577ab94..dda171d 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -3,6 +3,8 @@ from dataclasses import dataclass from enum import Enum, auto, unique +from .field_enums import AutoSleepMode, OutputMode + @unique class FieldType(Enum): @@ -20,6 +22,7 @@ class FieldAttributes: unit_of_measurement: str|None = None device_class: str|None = None state_class: str|None = None + options: Enum @dataclass(frozen=True) class PowerFieldAttributes(FieldAttributes): @@ -35,6 +38,13 @@ class VoltageFieldAttributes(FieldAttributes): device_class = 'voltage' state_class = 'measurement' +@dataclass +class CurrentFieldAttributes(FieldAttributes): + type = FieldType.NUMERIC + unit_of_measurement = 'A' + device_class = 'current' + state_class = 'measurement' + @dataclass(frozen=True) class OutletFieldAttributes(FieldAttributes): type = FieldType.BOOL @@ -65,7 +75,7 @@ FIELD_ATTRIBUTES = { type = FieldType.NUMERIC, name = 'Total Battery Percent', unit_of_measurement = '%', - device_class ='battery', + device_class = 'battery', state_class ='measurement', ), 'total_battery_voltage': VoltageFieldAttributes( @@ -87,6 +97,47 @@ FIELD_ATTRIBUTES = { ), # Device specific fields + 'ac_output_mode': FieldAttributes( + type = FieldType.ENUM, + name = 'AC Output Mode', + options = OutputMode, + ), + 'internal_ac_voltage': VoltageFieldAttributes( + name = 'Internal AC Voltage', + ), + 'internal_current_one': CurrentFieldAttributes( + name = 'Internal Current Sensor 1', + ), + 'internal_power_one': PowerFieldAttributes( + name = 'Internal Power Sensor 1', + ), + 'internal_ac_frequency': FieldAttributes( + type = FieldType.NUMERIC, + name = 'Internal AC Frequency', + unit_of_measurement = 'Hz', + device_class = 'frequency', + state_class ='measurement', + ), + 'internal_dc_input_voltage': VoltageFieldAttributes( + name = 'Internal DC Input Voltage', + ), + 'internal_dc_input_power': PowerFieldAttributes( + name = 'Internal DC Input Power', + ), + 'internal_dc_input_current': CurrentFieldAttributes( + name = 'Internal DC Input Current', + ), + 'power_off': FieldAttributes( + type = FieldType.BUTTON, + setter = True, + name = 'Power Off', + ), + 'auto_sleep_mode': FieldAttributes( + type = FieldType.ENUM, + setter = True, + name = 'Screen Auto Sleep Mode', + options = AutoSleepMode, + ), } def PACK_FIELD_ATTRIBUTES(pack: int): From 928ff2bf5db0b564bc8264e5bf90c305c576a161 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 20:09:59 +0100 Subject: [PATCH 20/82] added ac300 --- .../bluetti_bt_lib/devices/ac200m.py | 6 +- .../bluetti_bt_lib/devices/ac300.py | 68 +++++++++++++++ .../bluetti_bt_lib/field_attributes.py | 82 +++++++++++++++++-- .../bluetti_bt/bluetti_bt_lib/field_enums.py | 12 +++ 4 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index b2df3ee..f1193e6 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -31,18 +31,18 @@ class AC200M(ProtocolV1Device): @property def pack_num_max(self): return 3 - + @property def polling_commands(self) -> List[ReadHoldingRegisters]: return super().polling_commands + [ ReadHoldingRegisters(70, 4), ReadHoldingRegisters(86, 3), ] - + @property def writable_ranges(self) -> List[range]: return super().writable_ranges + [range(3060, 3061)] - + @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return super().pack_polling_commands + [ReadHoldingRegisters(105, 16)] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py new file mode 100644 index 0000000..8e61af9 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -0,0 +1,68 @@ +"""AC300 fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..field_enums import AutoSleepMode, OutputMode, SplitPhaseMachineType, UpsMode +from ..base_devices.ProtocolV1Device import ProtocolV1Device + + +class AC300(ProtocolV1Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "AC300", sn) + + # Details + self.struct.add_enum_field('ac_output_mode', 70, OutputMode) + self.struct.add_uint_field('internal_ac_voltage', 71) + self.struct.add_decimal_field('internal_current_one', 72, 1) + self.struct.add_uint_field('internal_power_one', 73) + self.struct.add_decimal_field('internal_ac_frequency', 74, 1) + self.struct.add_decimal_field('internal_current_two', 75, 1) + self.struct.add_uint_field('internal_power_two', 76) + self.struct.add_decimal_field('ac_input_voltage', 77, 1) + self.struct.add_decimal_field('internal_current_three', 78, 1, (0, 100)) + self.struct.add_uint_field('internal_power_three', 79) + self.struct.add_decimal_field('ac_input_frequency', 80, 2) + self.struct.add_uint_field('internal_dc_input_voltage', 86) + self.struct.add_decimal_field('internal_dc_input_power', 87, 1) + self.struct.add_decimal_field('internal_dc_input_current', 88, 2) + + # Battery packs + self.struct.add_decimal_array_field('cell_voltages', 105, 16, 2) # internal + self.struct.add_version_field('pack_bms_version', 201) + + # Controls + self.struct.add_enum_field('ups_mode', 3001, UpsMode) + self.struct.add_bool_field('split_phase_on', 3004) + self.struct.add_enum_field('split_phase_machine_mode', 3005, SplitPhaseMachineType) + self.struct.add_bool_field('grid_charge_on', 3011) + self.struct.add_bool_field('time_control_on', 3013) + self.struct.add_uint_field('battery_range_start', 3015) + self.struct.add_uint_field('battery_range_end', 3016) + self.struct.add_enum_field('auto_sleep_mode', 3061, AutoSleepMode) + + + @property + def pack_num_max(self): + return 4 + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(86, 3), + ] + + @property + def writable_ranges(self) -> List[range]: + return super().writable_ranges + [ + range(3001, 3016), + 3061, + ] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return super().pack_polling_commands + [ + ReadHoldingRegisters(105, 16), + ReadHoldingRegisters(201, 2), + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index dda171d..56fd903 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from enum import Enum, auto, unique -from .field_enums import AutoSleepMode, OutputMode +from .field_enums import AutoSleepMode, OutputMode, SplitPhaseMachineType, UpsMode @unique @@ -45,6 +45,13 @@ class CurrentFieldAttributes(FieldAttributes): device_class = 'current' state_class = 'measurement' +@dataclass +class FrequencyFieldAttributes(FieldAttributes): + type = FieldType.NUMERIC, + unit_of_measurement = 'Hz', + device_class = 'frequency', + state_class ='measurement', + @dataclass(frozen=True) class OutletFieldAttributes(FieldAttributes): type = FieldType.BOOL @@ -87,6 +94,8 @@ FIELD_ATTRIBUTES = { 'dc_output_on': OutletFieldAttributes( name = 'DC Output', ), + + # Controls 'ac_output_on_switch': OutletFieldAttributes( name = 'AC Output', setter = True, @@ -111,12 +120,26 @@ FIELD_ATTRIBUTES = { 'internal_power_one': PowerFieldAttributes( name = 'Internal Power Sensor 1', ), - 'internal_ac_frequency': FieldAttributes( - type = FieldType.NUMERIC, + 'internal_ac_frequency': FrequencyFieldAttributes( name = 'Internal AC Frequency', - unit_of_measurement = 'Hz', - device_class = 'frequency', - state_class ='measurement', + ), + 'internal_current_two': CurrentFieldAttributes( + name = 'Internal Current Sensor 2', + ), + 'internal_power_two': PowerFieldAttributes( + name = 'Internal Power Sensor 2', + ), + 'ac_input_voltage': VoltageFieldAttributes( + name = 'AC Input Voltage', + ), + 'internal_current_three': CurrentFieldAttributes( + name = 'Internal Current Sensor 3', + ), + 'internal_power_three': PowerFieldAttributes( + name = 'Internal Power Sensor 3', + ), + 'ac_input_frequency': FrequencyFieldAttributes( + name = 'AC Input Frequency', ), 'internal_dc_input_voltage': VoltageFieldAttributes( name = 'Internal DC Input Voltage', @@ -127,6 +150,8 @@ FIELD_ATTRIBUTES = { 'internal_dc_input_current': CurrentFieldAttributes( name = 'Internal DC Input Current', ), + + # Device specific controls 'power_off': FieldAttributes( type = FieldType.BUTTON, setter = True, @@ -138,6 +163,45 @@ FIELD_ATTRIBUTES = { name = 'Screen Auto Sleep Mode', options = AutoSleepMode, ), + 'ups_mode': FieldAttributes( + type = FieldType.ENUM, + setter = False, # Disabled for safety reasons + name = 'UPS Working Mode', + options = UpsMode, + ), + 'split_phase_on': FieldAttributes( + type = FieldType.BOOL, + setter = False, # Disabled for safety reasons + name = 'Split Phase', + ), + 'split_phase_machine_mode': FieldAttributes( + type = FieldType.ENUM, + setter = False, # Disabled for safety reasons + name = 'Split Phase Machine Type', + options = SplitPhaseMachineType, + ), + 'grid_charge_on': FieldAttributes( + type = FieldType.BOOL, + setter = False, # Disabled for safety reasons + name = 'Grid Charge', + ), + 'time_control_on': FieldAttributes( + type = FieldType.BOOL, + setter = False, # Disabled for safety reasons + name = 'Time Control', + ), + 'battery_range_start': FieldAttributes( + type = FieldType.NUMERIC, + setter = False, # Disabled + name = 'Battery Range Start', + unit_of_measurement = '%', + ), + 'battery_range_end': FieldAttributes( + type = FieldType.NUMERIC, + setter = False, # Disabled + name = 'Battery Range End', + unit_of_measurement = '%', + ), } def PACK_FIELD_ATTRIBUTES(pack: int): @@ -146,10 +210,14 @@ def PACK_FIELD_ATTRIBUTES(pack: int): name = f'Battery Pack {pack} Voltage', ), 'pack_battery_percent': FieldAttributes( - type = FieldType, + type = FieldType.NUMERIC, name = f'Battery Pack {pack} Percent', unit_of_measurement = '%', device_class ='battery', state_class ='measurement', ), + 'pack_bms_version': FieldAttributes( + type = FieldType.NUMERIC, + name = f'Battery Pack {pack} BMS Version', + ), } diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py index b1aecfc..8040c97 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py @@ -18,3 +18,15 @@ class AutoSleepMode(Enum): ONE_MINUTE = 3 FIVE_MINUTES = 4 NEVER = 5 + +@unique +class UpsMode(Enum): + CUSTOMIZED = 1 + PV_PRIORITY = 2 + STANDARD = 3 + TIME_CONTROL = 4 + +@unique +class SplitPhaseMachineType(Enum): + SLAVE = 0 + MASTER = 1 From 432d46d0f1f98dc7278497e55fa801edc70d9193 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 20:14:12 +0100 Subject: [PATCH 21/82] black code formatting --- .../base_devices/BluettiDevice.py | 4 +- .../base_devices/ProtocolV1Device.py | 40 +-- .../base_devices/ProtocolV2Device.py | 40 +-- .../bluetti_bt/bluetti_bt_lib/const.py | 6 +- .../bluetti_bt_lib/devices/ac200m.py | 22 +- .../bluetti_bt_lib/devices/ac300.py | 51 ++-- .../bluetti_bt/bluetti_bt_lib/exceptions.py | 1 + .../bluetti_bt_lib/field_attributes.py | 272 +++++++++--------- .../bluetti_bt/bluetti_bt_lib/field_enums.py | 2 + .../bluetti_bt_lib/utils/commands.py | 26 +- .../bluetti_bt/bluetti_bt_lib/utils/struct.py | 35 ++- 11 files changed, 257 insertions(+), 242 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py index 8ce7432..dcc5cd7 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py @@ -52,7 +52,9 @@ class BluettiDevice: def build_setter_command(self, field: str, value: Any): matches = [f for f in self.struct.fields if f.name == field] - device_field = next(f for f in matches if any(f.address in r for r in self.writable_ranges)) + device_field = next( + f for f in matches if any(f.address in r for r in self.writable_ranges) + ) # Convert value to an integer if isinstance(device_field, EnumField): diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index ba6ba2d..b7d717d 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -12,38 +12,40 @@ class ProtocolV1Device(BluettiDevice): self.struct = DeviceStruct() # Device info - self.struct.add_string_field('device_type', 10, 6) - self.struct.add_sn_field('serial_number', 17) + self.struct.add_string_field("device_type", 10, 6) + self.struct.add_sn_field("serial_number", 17) # Power IO - self.struct.add_uint_field('dc_input_power', 36) - self.struct.add_uint_field('ac_input_power', 37) - self.struct.add_uint_field('ac_output_power', 38) - self.struct.add_uint_field('dc_output_power', 39) + self.struct.add_uint_field("dc_input_power", 36) + self.struct.add_uint_field("ac_input_power", 37) + self.struct.add_uint_field("ac_output_power", 38) + self.struct.add_uint_field("dc_output_power", 39) # Power IO statistics - self.struct.add_decimal_field('power_generation', 41, 1) # Total power generated since last reset (kwh) + self.struct.add_decimal_field( + "power_generation", 41, 1 + ) # Total power generated since last reset (kwh) # Battery - self.struct.add_uint_field('total_battery_percent', 43) + self.struct.add_uint_field("total_battery_percent", 43) # Battery packs - self.struct.add_uint_field('pack_num_max', 91) # internal - self.struct.add_decimal_field('total_battery_voltage', 92, 1) - self.struct.add_uint_field('pack_num', 96) # internal - 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_num_max", 91) # internal + self.struct.add_decimal_field("total_battery_voltage", 92, 1) + self.struct.add_uint_field("pack_num", 96) # internal + self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage + self.struct.add_uint_field("pack_battery_percent", 99) # Output state - self.struct.add_bool_field('ac_output_on', 48) - self.struct.add_bool_field('dc_output_on', 49) + self.struct.add_bool_field("ac_output_on", 48) + self.struct.add_bool_field("dc_output_on", 49) # Pack selector - self.struct.add_uint_field('pack_num', 3006) # internal + self.struct.add_uint_field("pack_num", 3006) # internal # Output controls - self.struct.add_bool_field('ac_output_on_switch', 3007) - self.struct.add_bool_field('dc_output_on_switch', 3008) + self.struct.add_bool_field("ac_output_on_switch", 3007) + self.struct.add_bool_field("dc_output_on_switch", 3008) super().__init__(address, type, sn) @@ -63,7 +65,7 @@ class ProtocolV1Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: return [range(3006, 3008)] - + @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [ diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py index 21abd98..92f4808 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py @@ -12,38 +12,40 @@ class ProtocolV2Device(BluettiDevice): self.struct = DeviceStruct() # Device info - self.struct.add_string_field('device_type', 110, 6) - self.struct.add_sn_field('serial_number', 116) + self.struct.add_string_field("device_type", 110, 6) + self.struct.add_sn_field("serial_number", 116) # Power IO - #self.struct.add_uint_field('dc_input_power', ?) - #self.struct.add_uint_field('ac_input_power', ?) - #self.struct.add_uint_field('ac_output_power', ?) - #self.struct.add_uint_field('dc_output_power', ?) + # self.struct.add_uint_field('dc_input_power', ?) + # self.struct.add_uint_field('ac_input_power', ?) + # self.struct.add_uint_field('ac_output_power', ?) + # self.struct.add_uint_field('dc_output_power', ?) # Power IO statistics - self.struct.add_decimal_field('power_generation', 154, 1) # Total power generated since last reset (kwh) + self.struct.add_decimal_field( + "power_generation", 154, 1 + ) # Total power generated since last reset (kwh) # Battery - self.struct.add_uint_field('total_battery_percent', 102) + self.struct.add_uint_field("total_battery_percent", 102) # Battery packs - #self.struct.add_uint_field('pack_num_max', ?) # internal - #self.struct.add_decimal_field('total_battery_voltage', ?, 1) - #self.struct.add_uint_field('pack_num', ?) # internal - #self.struct.add_decimal_field('pack_voltage', ?, 2) # Full pack voltage - #self.struct.add_uint_field('pack_battery_percent', ?) + # self.struct.add_uint_field('pack_num_max', ?) # internal + # self.struct.add_decimal_field('total_battery_voltage', ?, 1) + # self.struct.add_uint_field('pack_num', ?) # internal + # self.struct.add_decimal_field('pack_voltage', ?, 2) # Full pack voltage + # self.struct.add_uint_field('pack_battery_percent', ?) # Output state - #self.struct.add_bool_field('ac_output_on', ?) - #self.struct.add_bool_field('dc_output_on', ?) + # self.struct.add_bool_field('ac_output_on', ?) + # self.struct.add_bool_field('dc_output_on', ?) # Pack selector - #self.struct.add_uint_field('pack_num', ?) # internal + # self.struct.add_uint_field('pack_num', ?) # internal # Output controls - #self.struct.add_bool_field('ac_output_on_switch', ?) - #self.struct.add_bool_field('dc_output_on_switch', ?) + # self.struct.add_bool_field('ac_output_on_switch', ?) + # self.struct.add_bool_field('dc_output_on_switch', ?) # Add battery fields here? @@ -61,7 +63,7 @@ class ProtocolV2Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: return [] - + @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: return [] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/const.py b/custom_components/bluetti_bt/bluetti_bt_lib/const.py index 7bc8f82..6524cad 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/const.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/const.py @@ -1,6 +1,6 @@ """Const definitions.""" RESPONSE_TIMEOUT = 5 -WRITE_UUID = '0000ff02-0000-1000-8000-00805f9b34fb' -NOTIFY_UUID = '0000ff01-0000-1000-8000-00805f9b34fb' -DEVICE_NAME_UUID = '00002a00-0000-1000-8000-00805f9b34fb' +WRITE_UUID = "0000ff02-0000-1000-8000-00805f9b34fb" +NOTIFY_UUID = "0000ff01-0000-1000-8000-00805f9b34fb" +DEVICE_NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb" diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index f1193e6..f5fa41a 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -12,21 +12,21 @@ class AC200M(ProtocolV1Device): super().__init__(address, "AC200M", sn) # Details - self.struct.add_enum_field('ac_output_mode', 70, OutputMode) - self.struct.add_uint_field('internal_ac_voltage', 71) - self.struct.add_decimal_field('internal_current_one', 72, 1) - self.struct.add_uint_field('internal_power_one', 73) - self.struct.add_decimal_field('internal_ac_frequency', 74, 1) - self.struct.add_uint_field('internal_dc_input_voltage', 86) - self.struct.add_decimal_field('internal_dc_input_power', 87, 1) - self.struct.add_decimal_field('internal_dc_input_current', 88, 2) + self.struct.add_enum_field("ac_output_mode", 70, OutputMode) + self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_current_one", 72, 1) + self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_decimal_field("internal_dc_input_power", 87, 1) + self.struct.add_decimal_field("internal_dc_input_current", 88, 2) # Battery packs - self.struct.add_decimal_array_field('cell_voltages', 105, 16, 2) # internal + self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal # Controls - self.struct.add_bool_field('power_off', 3060) - self.struct.add_enum_field('auto_sleep_mode', 3061, AutoSleepMode) + self.struct.add_bool_field("power_off", 3060) + self.struct.add_enum_field("auto_sleep_mode", 3061, AutoSleepMode) @property def pack_num_max(self): diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 8e61af9..bb757fc 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -12,35 +12,36 @@ class AC300(ProtocolV1Device): super().__init__(address, "AC300", sn) # Details - self.struct.add_enum_field('ac_output_mode', 70, OutputMode) - self.struct.add_uint_field('internal_ac_voltage', 71) - self.struct.add_decimal_field('internal_current_one', 72, 1) - self.struct.add_uint_field('internal_power_one', 73) - self.struct.add_decimal_field('internal_ac_frequency', 74, 1) - self.struct.add_decimal_field('internal_current_two', 75, 1) - self.struct.add_uint_field('internal_power_two', 76) - self.struct.add_decimal_field('ac_input_voltage', 77, 1) - self.struct.add_decimal_field('internal_current_three', 78, 1, (0, 100)) - self.struct.add_uint_field('internal_power_three', 79) - self.struct.add_decimal_field('ac_input_frequency', 80, 2) - self.struct.add_uint_field('internal_dc_input_voltage', 86) - self.struct.add_decimal_field('internal_dc_input_power', 87, 1) - self.struct.add_decimal_field('internal_dc_input_current', 88, 2) + self.struct.add_enum_field("ac_output_mode", 70, OutputMode) + self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_current_one", 72, 1) + self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_current_two", 75, 1) + self.struct.add_uint_field("internal_power_two", 76) + self.struct.add_decimal_field("ac_input_voltage", 77, 1) + self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) + self.struct.add_uint_field("internal_power_three", 79) + self.struct.add_decimal_field("ac_input_frequency", 80, 2) + self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_decimal_field("internal_dc_input_power", 87, 1) + self.struct.add_decimal_field("internal_dc_input_current", 88, 2) # Battery packs - self.struct.add_decimal_array_field('cell_voltages', 105, 16, 2) # internal - self.struct.add_version_field('pack_bms_version', 201) + self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + self.struct.add_version_field("pack_bms_version", 201) # Controls - self.struct.add_enum_field('ups_mode', 3001, UpsMode) - self.struct.add_bool_field('split_phase_on', 3004) - self.struct.add_enum_field('split_phase_machine_mode', 3005, SplitPhaseMachineType) - self.struct.add_bool_field('grid_charge_on', 3011) - self.struct.add_bool_field('time_control_on', 3013) - self.struct.add_uint_field('battery_range_start', 3015) - self.struct.add_uint_field('battery_range_end', 3016) - self.struct.add_enum_field('auto_sleep_mode', 3061, AutoSleepMode) - + self.struct.add_enum_field("ups_mode", 3001, UpsMode) + self.struct.add_bool_field("split_phase_on", 3004) + self.struct.add_enum_field( + "split_phase_machine_mode", 3005, SplitPhaseMachineType + ) + self.struct.add_bool_field("grid_charge_on", 3011) + self.struct.add_bool_field("time_control_on", 3013) + self.struct.add_uint_field("battery_range_start", 3015) + self.struct.add_uint_field("battery_range_end", 3016) + self.struct.add_enum_field("auto_sleep_mode", 3061, AutoSleepMode) @property def pack_num_max(self): diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py b/custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py index 6e708b9..ad81536 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/exceptions.py @@ -2,6 +2,7 @@ # Modified copy of https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/bluetooth/exc.py + class ParseError(Exception): pass diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 56fd903..cedeaf9 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -19,205 +19,209 @@ class FieldAttributes: type: FieldType setter: bool = False name: str - unit_of_measurement: str|None = None - device_class: str|None = None - state_class: str|None = None + unit_of_measurement: str | None = None + device_class: str | None = None + state_class: str | None = None options: Enum + @dataclass(frozen=True) class PowerFieldAttributes(FieldAttributes): type = FieldType.NUMERIC - unit_of_measurement = 'W' - device_class = 'power' - state_class = 'measurement' + unit_of_measurement = "W" + device_class = "power" + state_class = "measurement" + @dataclass class VoltageFieldAttributes(FieldAttributes): type = FieldType.NUMERIC - unit_of_measurement = 'V' - device_class = 'voltage' - state_class = 'measurement' + unit_of_measurement = "V" + device_class = "voltage" + state_class = "measurement" + @dataclass class CurrentFieldAttributes(FieldAttributes): type = FieldType.NUMERIC - unit_of_measurement = 'A' - device_class = 'current' - state_class = 'measurement' + unit_of_measurement = "A" + device_class = "current" + state_class = "measurement" + @dataclass class FrequencyFieldAttributes(FieldAttributes): - type = FieldType.NUMERIC, - unit_of_measurement = 'Hz', - device_class = 'frequency', - state_class ='measurement', + type = (FieldType.NUMERIC,) + unit_of_measurement = ("Hz",) + device_class = ("frequency",) + state_class = ("measurement",) + @dataclass(frozen=True) class OutletFieldAttributes(FieldAttributes): type = FieldType.BOOL - device_class = 'outlet' + device_class = "outlet" + FIELD_ATTRIBUTES = { # Base device fields - 'dc_input_power': PowerFieldAttributes( - name = 'DC Input Power', + "dc_input_power": PowerFieldAttributes( + name="DC Input Power", ), - 'ac_input_power': PowerFieldAttributes( - name = 'AC Input Power', + "ac_input_power": PowerFieldAttributes( + name="AC Input Power", ), - 'ac_output_power': PowerFieldAttributes( - name = 'AC Output Power', + "ac_output_power": PowerFieldAttributes( + name="AC Output Power", ), - 'dc_output_power': PowerFieldAttributes( - name = 'DC Output Power', + "dc_output_power": PowerFieldAttributes( + name="DC Output Power", ), - 'power_generation': FieldAttributes( - type = FieldType.NUMERIC, - name = 'Total Power Generation', - unit_of_measurement = 'kWh', - device_class = 'energy', - state_class = 'total_increasing', + "power_generation": FieldAttributes( + type=FieldType.NUMERIC, + name="Total Power Generation", + unit_of_measurement="kWh", + device_class="energy", + state_class="total_increasing", ), - 'total_battery_percent': FieldAttributes( - type = FieldType.NUMERIC, - name = 'Total Battery Percent', - unit_of_measurement = '%', - device_class = 'battery', - state_class ='measurement', + "total_battery_percent": FieldAttributes( + type=FieldType.NUMERIC, + name="Total Battery Percent", + unit_of_measurement="%", + device_class="battery", + state_class="measurement", ), - 'total_battery_voltage': VoltageFieldAttributes( - name = 'Total Battery Voltage', + "total_battery_voltage": VoltageFieldAttributes( + name="Total Battery Voltage", ), - 'ac_output_on': OutletFieldAttributes( - name = 'AC Output', + "ac_output_on": OutletFieldAttributes( + name="AC Output", ), - 'dc_output_on': OutletFieldAttributes( - name = 'DC Output', + "dc_output_on": OutletFieldAttributes( + name="DC Output", ), - # Controls - 'ac_output_on_switch': OutletFieldAttributes( - name = 'AC Output', - setter = True, + "ac_output_on_switch": OutletFieldAttributes( + name="AC Output", + setter=True, ), - 'dc_output_on_switch': OutletFieldAttributes( - name = 'DC Output', - setter = True, + "dc_output_on_switch": OutletFieldAttributes( + name="DC Output", + setter=True, ), - # Device specific fields - 'ac_output_mode': FieldAttributes( - type = FieldType.ENUM, - name = 'AC Output Mode', - options = OutputMode, + "ac_output_mode": FieldAttributes( + type=FieldType.ENUM, + name="AC Output Mode", + options=OutputMode, ), - 'internal_ac_voltage': VoltageFieldAttributes( - name = 'Internal AC Voltage', + "internal_ac_voltage": VoltageFieldAttributes( + name="Internal AC Voltage", ), - 'internal_current_one': CurrentFieldAttributes( - name = 'Internal Current Sensor 1', + "internal_current_one": CurrentFieldAttributes( + name="Internal Current Sensor 1", ), - 'internal_power_one': PowerFieldAttributes( - name = 'Internal Power Sensor 1', + "internal_power_one": PowerFieldAttributes( + name="Internal Power Sensor 1", ), - 'internal_ac_frequency': FrequencyFieldAttributes( - name = 'Internal AC Frequency', + "internal_ac_frequency": FrequencyFieldAttributes( + name="Internal AC Frequency", ), - 'internal_current_two': CurrentFieldAttributes( - name = 'Internal Current Sensor 2', + "internal_current_two": CurrentFieldAttributes( + name="Internal Current Sensor 2", ), - 'internal_power_two': PowerFieldAttributes( - name = 'Internal Power Sensor 2', + "internal_power_two": PowerFieldAttributes( + name="Internal Power Sensor 2", ), - 'ac_input_voltage': VoltageFieldAttributes( - name = 'AC Input Voltage', + "ac_input_voltage": VoltageFieldAttributes( + name="AC Input Voltage", ), - 'internal_current_three': CurrentFieldAttributes( - name = 'Internal Current Sensor 3', + "internal_current_three": CurrentFieldAttributes( + name="Internal Current Sensor 3", ), - 'internal_power_three': PowerFieldAttributes( - name = 'Internal Power Sensor 3', + "internal_power_three": PowerFieldAttributes( + name="Internal Power Sensor 3", ), - 'ac_input_frequency': FrequencyFieldAttributes( - name = 'AC Input Frequency', + "ac_input_frequency": FrequencyFieldAttributes( + name="AC Input Frequency", ), - 'internal_dc_input_voltage': VoltageFieldAttributes( - name = 'Internal DC Input Voltage', + "internal_dc_input_voltage": VoltageFieldAttributes( + name="Internal DC Input Voltage", ), - 'internal_dc_input_power': PowerFieldAttributes( - name = 'Internal DC Input Power', + "internal_dc_input_power": PowerFieldAttributes( + name="Internal DC Input Power", ), - 'internal_dc_input_current': CurrentFieldAttributes( - name = 'Internal DC Input Current', + "internal_dc_input_current": CurrentFieldAttributes( + name="Internal DC Input Current", ), - # Device specific controls - 'power_off': FieldAttributes( - type = FieldType.BUTTON, - setter = True, - name = 'Power Off', + "power_off": FieldAttributes( + type=FieldType.BUTTON, + setter=True, + name="Power Off", ), - 'auto_sleep_mode': FieldAttributes( - type = FieldType.ENUM, - setter = True, - name = 'Screen Auto Sleep Mode', - options = AutoSleepMode, + "auto_sleep_mode": FieldAttributes( + type=FieldType.ENUM, + setter=True, + name="Screen Auto Sleep Mode", + options=AutoSleepMode, ), - 'ups_mode': FieldAttributes( - type = FieldType.ENUM, - setter = False, # Disabled for safety reasons - name = 'UPS Working Mode', - options = UpsMode, + "ups_mode": FieldAttributes( + type=FieldType.ENUM, + setter=False, # Disabled for safety reasons + name="UPS Working Mode", + options=UpsMode, ), - 'split_phase_on': FieldAttributes( - type = FieldType.BOOL, - setter = False, # Disabled for safety reasons - name = 'Split Phase', + "split_phase_on": FieldAttributes( + type=FieldType.BOOL, + setter=False, # Disabled for safety reasons + name="Split Phase", ), - 'split_phase_machine_mode': FieldAttributes( - type = FieldType.ENUM, - setter = False, # Disabled for safety reasons - name = 'Split Phase Machine Type', - options = SplitPhaseMachineType, + "split_phase_machine_mode": FieldAttributes( + type=FieldType.ENUM, + setter=False, # Disabled for safety reasons + name="Split Phase Machine Type", + options=SplitPhaseMachineType, ), - 'grid_charge_on': FieldAttributes( - type = FieldType.BOOL, - setter = False, # Disabled for safety reasons - name = 'Grid Charge', + "grid_charge_on": FieldAttributes( + type=FieldType.BOOL, + setter=False, # Disabled for safety reasons + name="Grid Charge", ), - 'time_control_on': FieldAttributes( - type = FieldType.BOOL, - setter = False, # Disabled for safety reasons - name = 'Time Control', + "time_control_on": FieldAttributes( + type=FieldType.BOOL, + setter=False, # Disabled for safety reasons + name="Time Control", ), - 'battery_range_start': FieldAttributes( - type = FieldType.NUMERIC, - setter = False, # Disabled - name = 'Battery Range Start', - unit_of_measurement = '%', + "battery_range_start": FieldAttributes( + type=FieldType.NUMERIC, + setter=False, # Disabled + name="Battery Range Start", + unit_of_measurement="%", ), - 'battery_range_end': FieldAttributes( - type = FieldType.NUMERIC, - setter = False, # Disabled - name = 'Battery Range End', - unit_of_measurement = '%', + "battery_range_end": FieldAttributes( + type=FieldType.NUMERIC, + setter=False, # Disabled + name="Battery Range End", + unit_of_measurement="%", ), } + def PACK_FIELD_ATTRIBUTES(pack: int): return { - 'pack_voltage': VoltageFieldAttributes( - name = f'Battery Pack {pack} Voltage', + "pack_voltage": VoltageFieldAttributes( + name=f"Battery Pack {pack} Voltage", ), - 'pack_battery_percent': FieldAttributes( - type = FieldType.NUMERIC, - name = f'Battery Pack {pack} Percent', - unit_of_measurement = '%', - device_class ='battery', - state_class ='measurement', + "pack_battery_percent": FieldAttributes( + type=FieldType.NUMERIC, + name=f"Battery Pack {pack} Percent", + unit_of_measurement="%", + device_class="battery", + state_class="measurement", ), - 'pack_bms_version': FieldAttributes( - type = FieldType.NUMERIC, - name = f'Battery Pack {pack} BMS Version', + "pack_bms_version": FieldAttributes( + type=FieldType.NUMERIC, + name=f"Battery Pack {pack} BMS Version", ), } diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py index 8040c97..7d77776 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py @@ -19,6 +19,7 @@ class AutoSleepMode(Enum): FIVE_MINUTES = 4 NEVER = 5 + @unique class UpsMode(Enum): CUSTOMIZED = 1 @@ -26,6 +27,7 @@ class UpsMode(Enum): STANDARD = 3 TIME_CONTROL = 4 + @unique class SplitPhaseMachineType(Enum): SLAVE = 0 diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py index 87cad7b..e79191b 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/commands.py @@ -5,7 +5,7 @@ import struct import crcmod.predefined -modbus_crc = crcmod.predefined.mkCrcFun('modbus') +modbus_crc = crcmod.predefined.mkCrcFun("modbus") class DeviceCommand: @@ -16,7 +16,7 @@ class DeviceCommand: self.cmd[0] = 1 # MODBUS address self.cmd[1] = function_code self.cmd[2:-2] = data - struct.pack_into(' int: """Returns the expected response size in bytes""" @@ -39,7 +39,7 @@ class DeviceCommand: return False crc = modbus_crc(response[0:-2]) - crc_bytes = crc.to_bytes(2, byteorder='little') + crc_bytes = crc.to_bytes(2, byteorder="little") return response[-2:] == crc_bytes def parse_response(self, response: bytes): @@ -52,7 +52,7 @@ class ReadHoldingRegisters(DeviceCommand): self.starting_address = starting_address self.quantity = quantity - super().__init__(3, struct.pack('!HH', starting_address, quantity)) + super().__init__(3, struct.pack("!HH", starting_address, quantity)) def response_size(self): # 3 byte header @@ -64,9 +64,7 @@ class ReadHoldingRegisters(DeviceCommand): return bytes(response[3:-2]) def __repr__(self): - return ( - f'ReadHoldingRegisters(starting_address={self.starting_address}, quantity={self.quantity})' - ) + return f"ReadHoldingRegisters(starting_address={self.starting_address}, quantity={self.quantity})" class WriteSingleRegister(DeviceCommand): @@ -74,7 +72,7 @@ class WriteSingleRegister(DeviceCommand): self.address = address self.value = value - super().__init__(6, struct.pack('!HH', address, value)) + super().__init__(6, struct.pack("!HH", address, value)) def response_size(self): return 8 @@ -83,22 +81,20 @@ class WriteSingleRegister(DeviceCommand): return bytes(response[4:6]) def __repr__(self): - return ( - f'WriteSingleRegister(address={self.address}, value={self.value:#04x})' - ) + return f"WriteSingleRegister(address={self.address}, value={self.value:#04x})" class WriteMultipleRegisters(DeviceCommand): def __init__(self, starting_address: int, data: bytes): if len(data) % 2 != 0: - raise ValueError('data size must be multiple of 2') + raise ValueError("data size must be multiple of 2") self.starting_address = starting_address self.data = data body = bytearray(len(data) + 5) half_len = len(data) >> 1 - struct.pack_into('!HHB', body, 0, starting_address, half_len, len(data)) + struct.pack_into("!HHB", body, 0, starting_address, half_len, len(data)) body[5:] = data super().__init__(16, body) @@ -106,6 +102,4 @@ class WriteMultipleRegisters(DeviceCommand): return 8 def __repr__(self): - return ( - f'WriteMultipleRegisters(starting_address={self.starting_address}, data={self.data})' - ) + return f"WriteMultipleRegisters(starting_address={self.starting_address}, data={self.data})" diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index 9e9f27b..ea9418f 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -35,7 +35,7 @@ class UintField(DeviceField): super().__init__(name, address, 1) def parse(self, data: bytes) -> int: - return struct.unpack('!H', data)[0] + return struct.unpack("!H", data)[0] def in_range(self, val: int) -> bool: if self.range is None: @@ -49,7 +49,7 @@ class BoolField(DeviceField): super().__init__(name, address, 1) def parse(self, data: bytes) -> bool: - return struct.unpack('!H', data)[0] == 1 + return struct.unpack("!H", data)[0] == 1 class EnumField(DeviceField): @@ -58,18 +58,20 @@ class EnumField(DeviceField): super().__init__(name, address, 1) def parse(self, data: bytes) -> Any: - val = struct.unpack('!H', data)[0] + val = struct.unpack("!H", data)[0] return self.enum(val) class DecimalField(DeviceField): - def __init__(self, name: str, address: int, scale: int, range: Optional[Tuple[int, int]]): + def __init__( + self, name: str, address: int, scale: int, range: Optional[Tuple[int, int]] + ): self.scale = scale self.range = range super().__init__(name, address, 1) 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 def in_range(self, val: Decimal) -> bool: @@ -85,20 +87,22 @@ class DecimalArrayField(DeviceField): super().__init__(name, address, size) def parse(self, data: bytes) -> Decimal: - values = list(struct.unpack(f'!{self.size}H', data)) + values = list(struct.unpack(f"!{self.size}H", data)) return [Decimal(v) / 10 ** self.scale for v in values] class StringField(DeviceField): """Fixed-width null-terminated string field""" + def parse(self, data: bytes) -> str: - return data.rstrip(b'\0').decode('ascii') + return data.rstrip(b"\0").decode("ascii") class SwapStringField(DeviceField): """Fixed-width null-terminated string field""" + def parse(self, data: bytes) -> str: - return swap_bytes(data).rstrip(b'\0').decode('ascii') + return swap_bytes(data).rstrip(b"\0").decode("ascii") class VersionField(DeviceField): @@ -106,7 +110,7 @@ class VersionField(DeviceField): super().__init__(name, address, 2) def parse(self, data: bytes) -> int: - values = struct.unpack('!2H', data) + values = struct.unpack("!2H", data) return Decimal(values[0] + (values[1] << 16)) / 100 @@ -115,7 +119,7 @@ class SerialNumberField(DeviceField): super().__init__(name, address, 4) def parse(self, data: bytes) -> int: - values = struct.unpack('!4H', data) + values = struct.unpack("!4H", data) return values[0] + (values[1] << 16) + (values[2] << 32) + (values[3] << 48) @@ -134,7 +138,9 @@ class DeviceStruct: def add_enum_field(self, name: str, address: int, enum: Type[Enum]): self.fields.append(EnumField(name, address, enum)) - def add_decimal_field(self, name: str, address: int, scale: int, range: Tuple[int, int] = None): + def add_decimal_field( + self, name: str, address: int, scale: int, range: Tuple[int, int] = None + ): self.fields.append(DecimalField(name, address, scale, range)) def add_decimal_array_field(self, name: str, address: int, size: int, scale: int): @@ -159,14 +165,15 @@ class DeviceStruct: # Filter out fields not in range r = range(starting_address, starting_address + data_size) - fields = [f for f in self.fields - if f.address in r and f.address + f.size - 1 in r] + fields = [ + f for f in self.fields if f.address in r and f.address + f.size - 1 in r + ] # Parse fields parsed = {} for f in fields: data_start = 2 * (f.address - starting_address) - field_data = data[data_start:data_start + 2 * f.size] + field_data = data[data_start : data_start + 2 * f.size] val = f.parse(field_data) # Skip if the value is "out-of-range" - sometimes the sensors From a183e8b02fe25d56114032e1ca19182da491610f Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 20:19:19 +0100 Subject: [PATCH 22/82] make options optional --- custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index cedeaf9..79ad4b4 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -22,7 +22,7 @@ class FieldAttributes: unit_of_measurement: str | None = None device_class: str | None = None state_class: str | None = None - options: Enum + options: Enum | None = None @dataclass(frozen=True) From 3d65d8b00f7e71964ad0da555222dc9cede1fdd9 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 20:24:44 +0100 Subject: [PATCH 23/82] added ac500 --- .../bluetti_bt_lib/devices/ac500.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py new file mode 100644 index 0000000..ed3e44c --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -0,0 +1,67 @@ +"""AC500 fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..field_enums import AutoSleepMode, OutputMode, SplitPhaseMachineType, UpsMode +from ..base_devices.ProtocolV1Device import ProtocolV1Device + + +class AC500(ProtocolV1Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "AC500", sn) + + # Details + self.struct.add_enum_field("ac_output_mode", 70, OutputMode) + self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_current_one", 72, 1) + self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_current_two", 75, 1) + self.struct.add_uint_field("internal_power_two", 76) + self.struct.add_decimal_field("ac_input_voltage", 77, 1) + self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) + self.struct.add_uint_field("internal_power_three", 79) + self.struct.add_decimal_field("ac_input_frequency", 80, 2) + self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_decimal_field("internal_dc_input_power", 87, 1) + self.struct.add_decimal_field("internal_dc_input_current", 88, 2) + + # Battery packs + self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + + # Controls + self.struct.add_enum_field("ups_mode", 3001, UpsMode) + self.struct.add_bool_field("split_phase_on", 3004) + self.struct.add_enum_field( + "split_phase_machine_mode", 3005, SplitPhaseMachineType + ) + self.struct.add_bool_field("grid_charge_on", 3011) + self.struct.add_bool_field("time_control_on", 3013) + self.struct.add_uint_field("battery_range_start", 3015) + self.struct.add_uint_field("battery_range_end", 3016) + self.struct.add_enum_field("auto_sleep_mode", 3061, AutoSleepMode) + + @property + def pack_num_max(self): + return 4 + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(86, 3), + ] + + @property + def writable_ranges(self) -> List[range]: + return super().writable_ranges + [ + range(3001, 3016), + 3061, + ] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return super().pack_polling_commands + [ + ReadHoldingRegisters(105, 16), + ] From e395c400b50ecfb7256b35f8d5058de2a3dec5d4 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 20:36:32 +0100 Subject: [PATCH 24/82] added eb3a --- .../bluetti_bt/bluetti_bt_lib/devices/eb3a.py | 39 +++++++++++++++++++ .../bluetti_bt_lib/field_attributes.py | 38 +++++++++++++++++- .../bluetti_bt/bluetti_bt_lib/field_enums.py | 23 +++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py new file mode 100644 index 0000000..6f37a66 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py @@ -0,0 +1,39 @@ +"""EB3A fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..field_enums import ChargingMode, EcoShutdown, LedMode +from ..base_devices.ProtocolV1Device import ProtocolV1Device + + +class EB3A(ProtocolV1Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "EB3A", sn) + + # Details + self.struct.add_decimal_field("ac_input_voltage", 77, 1) + self.struct.add_uint_field("internal_dc_input_voltage", 86) + + # Controls + self.struct.add_enum_field("led_mode", 3034, LedMode) + self.struct.add_bool_field("power_off", 3060) + self.struct.add_bool_field("eco_on", 3063) + self.struct.add_enum_field("eco_shutdown", 3064, EcoShutdown) + self.struct.add_enum_field("charging_mode", 3065, ChargingMode) + self.struct.add_bool_field("power_lifting_on", 3066) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(77, 1), + ReadHoldingRegisters(86, 1), + ] + + @property + def writable_ranges(self) -> List[range]: + return super().writable_ranges + [ + 3034, + 3060, + range(3063, 3066), + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 79ad4b4..7414323 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -3,7 +3,15 @@ from dataclasses import dataclass from enum import Enum, auto, unique -from .field_enums import AutoSleepMode, OutputMode, SplitPhaseMachineType, UpsMode +from .field_enums import ( + AutoSleepMode, + ChargingMode, + EcoShutdown, + LedMode, + OutputMode, + SplitPhaseMachineType, + UpsMode, +) @unique @@ -205,6 +213,34 @@ FIELD_ATTRIBUTES = { name="Battery Range End", unit_of_measurement="%", ), + "led_mode": FieldAttributes( + type=FieldType.ENUM, + setter=True, + name="LED Mode", + options=LedMode, + ), + "eco_on": FieldAttributes( + type=FieldType.BOOL, + setter=True, + name="Eco Mode", + ), + "eco_shutdown": FieldAttributes( + type=FieldType.ENUM, + setter=True, + name="Eco Shutdown", + options=EcoShutdown, + ), + "charging_mode": FieldAttributes( + type=FieldType.ENUM, + setter=True, + name="Charging Mode", + options=ChargingMode, + ), + "power_lifting_on": FieldAttributes( + type=FieldType.BOOL, + setter=False, # Disabled for safety reasons + name="Power Lifting", + ), } diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py index 7d77776..e74b22a 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_enums.py @@ -32,3 +32,26 @@ class UpsMode(Enum): class SplitPhaseMachineType(Enum): SLAVE = 0 MASTER = 1 + + +@unique +class LedMode(Enum): + LOW = 1 + HIGH = 2 + SOS = 3 + OFF = 4 + + +@unique +class EcoShutdown(Enum): + ONE_HOUR = 1 + TWO_HOURS = 2 + THREE_HOURS = 3 + FOUR_HOURS = 4 + + +@unique +class ChargingMode(Enum): + STANDARD = 0 + SILENT = 1 + TURBO = 2 From 3a709f9bc5ed18cd3c606d6d9b56fea1f580ba3f Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 20:43:34 +0100 Subject: [PATCH 25/82] added ep500 --- .../bluetti_bt_lib/devices/ac300.py | 4 +- .../bluetti_bt_lib/devices/ac500.py | 6 +- .../bluetti_bt_lib/devices/ep500.py | 63 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index bb757fc..5151c32 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -11,7 +11,7 @@ class AC300(ProtocolV1Device): def __init__(self, address: str, sn: str): super().__init__(address, "AC300", sn) - # Details + # Details (matches EP500 + AC500) self.struct.add_enum_field("ac_output_mode", 70, OutputMode) self.struct.add_uint_field("internal_ac_voltage", 71) self.struct.add_decimal_field("internal_current_one", 72, 1) @@ -31,7 +31,7 @@ class AC300(ProtocolV1Device): self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_version_field("pack_bms_version", 201) - # Controls + # Controls (matches EP500 + AC500) self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_enum_field( diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index ed3e44c..12edcb2 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -11,7 +11,7 @@ class AC500(ProtocolV1Device): def __init__(self, address: str, sn: str): super().__init__(address, "AC500", sn) - # Details + # Details (matches EP500 + AC300) self.struct.add_enum_field("ac_output_mode", 70, OutputMode) self.struct.add_uint_field("internal_ac_voltage", 71) self.struct.add_decimal_field("internal_current_one", 72, 1) @@ -27,10 +27,10 @@ class AC500(ProtocolV1Device): self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) - # Battery packs + # Battery packs (matches EP500) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal - # Controls + # Controls (matches EP500 + AC300) self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_enum_field( diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py new file mode 100644 index 0000000..1c5ff4a --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -0,0 +1,63 @@ +"""EP500 fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..field_enums import AutoSleepMode, OutputMode, SplitPhaseMachineType, UpsMode +from ..base_devices.ProtocolV1Device import ProtocolV1Device + + +class EP500(ProtocolV1Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "EP500", sn) + + # Details (matches AC500 + AC300) + self.struct.add_enum_field("ac_output_mode", 70, OutputMode) + self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_current_one", 72, 1) + self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_current_two", 75, 1) + self.struct.add_uint_field("internal_power_two", 76) + self.struct.add_decimal_field("ac_input_voltage", 77, 1) + self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) + self.struct.add_uint_field("internal_power_three", 79) + self.struct.add_decimal_field("ac_input_frequency", 80, 2) + self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_decimal_field("internal_dc_input_power", 87, 1) + self.struct.add_decimal_field("internal_dc_input_current", 88, 2) + + # Battery packs (matches AC500) + self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + + # Controls (matches AC500 + AC300) + self.struct.add_enum_field("ups_mode", 3001, UpsMode) + self.struct.add_bool_field("split_phase_on", 3004) + self.struct.add_enum_field( + "split_phase_machine_mode", 3005, SplitPhaseMachineType + ) + self.struct.add_bool_field("grid_charge_on", 3011) + self.struct.add_bool_field("time_control_on", 3013) + self.struct.add_uint_field("battery_range_start", 3015) + self.struct.add_uint_field("battery_range_end", 3016) + self.struct.add_enum_field("auto_sleep_mode", 3061, AutoSleepMode) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(86, 3), + ] + + @property + def writable_ranges(self) -> List[range]: + return super().writable_ranges + [ + range(3001, 3016), + 3061, + ] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return super().pack_polling_commands + [ + ReadHoldingRegisters(105, 16), + ] From 03b11a69d5f8b3013dd0e26c7ccfafcc7f577c9c Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 20:46:20 +0100 Subject: [PATCH 26/82] added ep500p --- .../bluetti_bt_lib/devices/ep500p.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py new file mode 100644 index 0000000..f70d906 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -0,0 +1,63 @@ +"""EP500P fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..field_enums import AutoSleepMode, OutputMode, SplitPhaseMachineType, UpsMode +from ..base_devices.ProtocolV1Device import ProtocolV1Device + + +class EP500P(ProtocolV1Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "EP500P", sn) + + # Details (matches AC500 + AC300) + self.struct.add_enum_field("ac_output_mode", 70, OutputMode) + self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_current_one", 72, 1) + self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_current_two", 75, 1) + self.struct.add_uint_field("internal_power_two", 76) + self.struct.add_decimal_field("ac_input_voltage", 77, 1) + self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) + self.struct.add_uint_field("internal_power_three", 79) + self.struct.add_decimal_field("ac_input_frequency", 80, 2) + self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_decimal_field("internal_dc_input_power", 87, 1) + self.struct.add_decimal_field("internal_dc_input_current", 88, 2) + + # Battery packs (matches AC500) + self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + + # Controls (matches AC500 + AC300) + self.struct.add_enum_field("ups_mode", 3001, UpsMode) + self.struct.add_bool_field("split_phase_on", 3004) + self.struct.add_enum_field( + "split_phase_machine_mode", 3005, SplitPhaseMachineType + ) + self.struct.add_bool_field("grid_charge_on", 3011) + self.struct.add_bool_field("time_control_on", 3013) + self.struct.add_uint_field("battery_range_start", 3015) + self.struct.add_uint_field("battery_range_end", 3016) + self.struct.add_enum_field("auto_sleep_mode", 3061, AutoSleepMode) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(86, 3), + ] + + @property + def writable_ranges(self) -> List[range]: + return super().writable_ranges + [ + range(3001, 3016), + 3061, + ] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return super().pack_polling_commands + [ + ReadHoldingRegisters(105, 16), + ] From d8c61e1ebeaf8ef955a2eb6b8340c7e4d8a13023 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 17 Feb 2024 21:23:08 +0100 Subject: [PATCH 27/82] added ep600 --- .../bluetti_bt_lib/devices/ep600.py | 98 +++++++++++++++ .../bluetti_bt_lib/field_attributes.py | 119 +++++++++++++++++- 2 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py new file mode 100644 index 0000000..8c9a808 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py @@ -0,0 +1,98 @@ +"""EP600 fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..base_devices.ProtocolV2Device import ProtocolV2Device + + +class EP600(ProtocolV2Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "EP600", sn) + + # Details + self.struct.add_uint_field("battery_range_start", 2022) + self.struct.add_uint_field("battery_range_end", 2023) + self.struct.add_uint_field("max_ac_input_power", 2213) + self.struct.add_uint_field("max_ac_input_current", 2214) + self.struct.add_uint_field("max_ac_output_power", 2215) + self.struct.add_uint_field("max_ac_output_current", 2216) + + # DC Solar + self.struct.add_uint_field("pv_input_power1", 1212) + self.struct.add_uint_field("pv_input_voltage1", 1213) + self.struct.add_uint_field("pv_input_current1", 1214) + self.struct.add_uint_field("pv_input_power2", 1220) + self.struct.add_uint_field("pv_input_voltage2", 1221) + self.struct.add_uint_field("pv_input_current2", 1222) + + # 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_phase2", 1236) + self.struct.add_uint_field("adl400_ac_input_power_phase3", 1244) + self.struct.add_decimal_field("adl400_ac_input_voltage_phase1", 1229, 1) + self.struct.add_decimal_field("adl400_ac_input_voltage_phase2", 1237, 1) + self.struct.add_decimal_field("adl400_ac_input_voltage_phase3", 1245, 1) + # Testing required + self.struct.add_uint_field("adl400_ac_input_current_phase1", 1230) + self.struct.add_uint_field("adl400_ac_input_current_phase2", 1238) + self.struct.add_uint_field("adl400_ac_input_current_phase3", 1246) + + # Grid Input + self.struct.add_decimal_field("grid_frequency", 1300, 1) + self.struct.add_uint_field("grid_power_phase1", 1313) + self.struct.add_uint_field("grid_power_phase2", 1319) + self.struct.add_uint_field("grid_power_phase3", 1325) + self.struct.add_decimal_field("grid_voltage_phase1", 1314, 1) + self.struct.add_decimal_field("grid_voltage_phase2", 1320, 1) + self.struct.add_decimal_field("grid_voltage_phase3", 1326, 1) + self.struct.add_decimal_field("grid_current_phase1", 1315, 1) + self.struct.add_decimal_field("grid_current_phase2", 1321, 1) + self.struct.add_decimal_field("grid_current_phase3", 1327, 1) + + # EP600 AC Output + self.struct.add_decimal_field("ac_output_frequency", 1500, 1) + self.struct.add_uint_field("ac_output_power_phase1", 1510) + self.struct.add_uint_field("ac_output_power_phase2", 1517) + self.struct.add_uint_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_phase2", 1518, 1) + self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1) + self.struct.add_decimal_field("ac_output_current_phase1", 1512, 1) + self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1) + self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1) + + # Statistics + self.struct.add_decimal_field( + "total_ac_consumption", 152, 1 + ) # Load consumption + self.struct.add_decimal_field( + "total_grid_consumption", 156, 1 + ) # Grid consumption stats + self.struct.add_decimal_field("total_grid_feed", 158, 1) + + # Battery packs + self.struct.add_swap_string_field("battery_type", 6101, 6) # internal + self.struct.add_sn_field("battery_serial_number", 6107) # internal + self.struct.add_version_field("bcu_version", 6175) # internal + self.struct.add_version_field("bmu_version", 6178) # internal + self.struct.add_version_field("safety_module_version", 6181) # internal + self.struct.add_version_field("high_voltage_module_version", 6184) # internal + + # @property + # def pack_num_max(self): + # return 16 + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(2022, 2), + ReadHoldingRegisters(2213, 5), + ] + + # @property + # def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + # return super().pack_polling_commands + [ + # ReadHoldingRegisters(6101, 10), + # ReadHoldingRegisters(6175, 12), + # ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 7414323..0b9516d 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -57,6 +57,14 @@ class CurrentFieldAttributes(FieldAttributes): state_class = "measurement" +@dataclass +class EnergyFieldAttributes(FieldAttributes): + type = FieldType.NUMERIC + unit_of_measurement = "kWh" + device_class = "energy" + state_class = "total_increasing" + + @dataclass class FrequencyFieldAttributes(FieldAttributes): type = (FieldType.NUMERIC,) @@ -85,12 +93,8 @@ FIELD_ATTRIBUTES = { "dc_output_power": PowerFieldAttributes( name="DC Output Power", ), - "power_generation": FieldAttributes( - type=FieldType.NUMERIC, + "power_generation": EnergyFieldAttributes( name="Total Power Generation", - unit_of_measurement="kWh", - device_class="energy", - state_class="total_increasing", ), "total_battery_percent": FieldAttributes( type=FieldType.NUMERIC, @@ -162,6 +166,111 @@ FIELD_ATTRIBUTES = { "internal_dc_input_current": CurrentFieldAttributes( name="Internal DC Input Current", ), + "pv_input_power1": PowerFieldAttributes( + name="Solar Input Power 1", + ), + "pv_input_power2": PowerFieldAttributes( + name="Solar Input Power 2", + ), + "pv_input_voltage1": VoltageFieldAttributes( + name="Solar Input Voltage 1", + ), + "pv_input_voltage2": VoltageFieldAttributes( + name="Solar Input Voltage 2", + ), + "pv_input_current1": CurrentFieldAttributes( + name="Solar Input Current 1", + ), + "pv_input_current2": CurrentFieldAttributes( + name="Solar Input Current 2", + ), + "adl400_ac_input_power_phase1": PowerFieldAttributes( + name="AC Solar Input Power Phase 1" + ), + "adl400_ac_input_power_phase2": PowerFieldAttributes( + name="AC Solar Input Power Phase 2" + ), + "adl400_ac_input_power_phase3": PowerFieldAttributes( + name="AC Solar Input Power Phase 3" + ), + "adl400_ac_input_voltage_phase1": VoltageFieldAttributes( + name="AC Solar Input Voltage Phase 1", + ), + "adl400_ac_input_voltage_phase2": VoltageFieldAttributes( + name="AC Solar Input Voltage Phase 2", + ), + "adl400_ac_input_voltage_phase3": VoltageFieldAttributes( + name="AC Solar Input Voltage Phase 3", + ), + "adl400_ac_input_current_phase1": CurrentFieldAttributes( + name="AC Solar Input Current Phase 1", + ), + "adl400_ac_input_current_phase2": CurrentFieldAttributes( + name="AC Solar Input Current Phase 2", + ), + "adl400_ac_input_current_phase3": CurrentFieldAttributes( + name="AC Solar Input Current Phase 3", + ), + "grid_frequency": FrequencyFieldAttributes( + name="Grid Frequency", + ), + "grid_power_phase1": PowerFieldAttributes( + name="Grid Power Phase 1", + ), + "grid_power_phase2": PowerFieldAttributes( + name="Grid Power Phase 2", + ), + "grid_power_phase3": PowerFieldAttributes( + name="Grid Power Phase 3", + ), + "grid_voltage_phase1": VoltageFieldAttributes( + name="Grid Voltage Phase 1", + ), + "grid_voltage_phase2": VoltageFieldAttributes( + name="Grid Voltage Phase 2", + ), + "grid_voltage_phase3": VoltageFieldAttributes( + name="Grid Voltage Phase 3", + ), + "grid_current_phase1": CurrentFieldAttributes( + name="Grid Current Phase 1", + ), + "grid_current_phase2": CurrentFieldAttributes( + name="Grid Current Phase 2", + ), + "grid_current_phase3": CurrentFieldAttributes( + name="Grid Current Phase 3", + ), + "ac_output_frequency": FrequencyFieldAttributes( + name="AC Output Frequency", + ), + "ac_output_power_phase1": PowerFieldAttributes( + name="AC Output Power Phase 1", + ), + "ac_output_power_phase2": PowerFieldAttributes( + name="AC Output Power Phase 2", + ), + "ac_output_power_phase3": PowerFieldAttributes( + name="AC Output Power Phase 3", + ), + "ac_output_voltage_phase1": VoltageFieldAttributes( + name="AC Output Voltage Phase 1", + ), + "ac_output_voltage_phase2": VoltageFieldAttributes( + name="AC Output Voltage Phase 2", + ), + "ac_output_voltage_phase3": VoltageFieldAttributes( + name="AC Output Voltage Phase 3", + ), + "total_ac_consumption": EnergyFieldAttributes( + name="Total Load Consumption", + ), + "total_grid_consumption": EnergyFieldAttributes( + name="Total Grid Consumption", + ), + "total_grid_feed": EnergyFieldAttributes( + name="Total Grid Feed", + ), # Device specific controls "power_off": FieldAttributes( type=FieldType.BUTTON, From 71bf9b195252b723459314130ee11d88416db6c1 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 13:19:02 +0100 Subject: [PATCH 28/82] removed bluetti_mqtt requirement --- README.md | 6 ------ custom_components/bluetti_bt/manifest.json | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 38e5292..b0a50be 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ Bluetti Integration for Home Assistant -Based on [bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt). - ## Disclaimer This integration is provided without any warranty or support by Bluetti (unfortunately). I do not take responsibility for any problems it may cause in all cases. Use it at your own risk. @@ -17,7 +15,6 @@ After the installation, you can use this button to install the integration: [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=Patrick762&repository=hassio-bluetti-bt&category=integration) ### Supported devices: -(based on [supported devices of bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt/tree/main/bluetti_mqtt/core/devices)) - AC60 - AC200M @@ -29,9 +26,6 @@ After the installation, you can use this button to install the integration: - EP600 (tested) - EP760 (beta, untested) -### Available sensors: -All sensors which are available in bluetti_mqtt (Based on [this file](https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/mqtt_client.py)). - ### Available controls: If enabled in the Integration options (you need to reload the integration if you change this option): AC and DC outputs diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index c79a35a..ff05ddb 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -19,6 +19,6 @@ "integration_type": "device", "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", - "requirements": ["bluetti_mqtt==0.15.0"], + "requirements": [], "version": "0.0.16" } From 2abcbbb39254c96bd12c15629ce90d6f9ac3bfa3 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 13:29:20 +0100 Subject: [PATCH 29/82] added build_device() --- custom_components/bluetti_bt/__init__.py | 3 +- .../bluetti_bt_lib/utils/device_builder.py | 34 ++++++ custom_components/bluetti_bt/devices/ep600.py | 113 ------------------ custom_components/bluetti_bt/utils.py | 6 +- 4 files changed, 38 insertions(+), 118 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py delete mode 100644 custom_components/bluetti_bt/devices/ep600.py diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 9ef157b..46be53c 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import re import logging +from typing import List from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry @@ -25,7 +26,7 @@ from .const import ( ) from .coordinator import PollingCoordinator -PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: List[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py new file mode 100644 index 0000000..f549558 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -0,0 +1,34 @@ +"""Device builder functions.""" + +import re + +from ..devices.ac60 import AC60 +from ..devices.ac200m import AC200M +from ..devices.ac300 import AC300 +from ..devices.ac500 import AC500 +from ..devices.eb3a import EB3A +from ..devices.ep500 import EP500 +from ..devices.ep500p import EP500P +from ..devices.ep600 import EP600 + +DEVICE_NAME_RE = re.compile(r"^(AC60|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600)(\d+)$") + + +def build_device(address: str, name: str): + match = DEVICE_NAME_RE.match(name) + if match[1] == "AC60": + return AC60(address, match[2]) + if match[1] == "AC200M": + return AC200M(address, match[2]) + if match[1] == "AC300": + return AC300(address, match[2]) + if match[1] == "AC500": + return AC500(address, match[2]) + if match[1] == "EB3A": + return EB3A(address, match[2]) + if match[1] == "EP500": + return EP500(address, match[2]) + if match[1] == "EP500P": + return EP500P(address, match[2]) + if match[1] == "EP600": + return EP600(address, match[2]) diff --git a/custom_components/bluetti_bt/devices/ep600.py b/custom_components/bluetti_bt/devices/ep600.py deleted file mode 100644 index 3ea91a7..0000000 --- a/custom_components/bluetti_bt/devices/ep600.py +++ /dev/null @@ -1,113 +0,0 @@ -"""EP600 Field definitions. - -Changes made here are only temporary and should also be -contributed to https://github.com/warhammerkid/bluetti_mqtt -""" - -from typing import List - -from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice -from bluetti_mqtt.core.devices.struct import DeviceStruct -from bluetti_mqtt.core.commands import ReadHoldingRegisters - -class EP600(BluettiDevice): - def __init__(self, address: str, sn: str): - self.struct = DeviceStruct() - - self.struct.add_uint_field('total_battery_percent', 102) - self.struct.add_swap_string_field('device_type', 110, 6) - self.struct.add_sn_field('serial_number', 116) - self.struct.add_decimal_field('power_generation', 154, 1) # Total power generated since last reset (kwh) - self.struct.add_swap_string_field('device_type', 1101, 6) - self.struct.add_sn_field('serial_number', 1107) - self.struct.add_decimal_field('power_generation', 1202, 1) # Total power generated since last reset (kwh) - self.struct.add_uint_field('battery_range_start', 2022) - self.struct.add_uint_field('battery_range_end', 2023) - self.struct.add_uint_field('max_ac_input_power', 2213) - self.struct.add_uint_field('max_ac_input_current', 2214) - self.struct.add_uint_field('max_ac_output_power', 2215) - self.struct.add_uint_field('max_ac_output_current', 2216) - self.struct.add_swap_string_field('battery_type', 6101, 6) - self.struct.add_sn_field('battery_serial_number', 6107) - self.struct.add_version_field('bcu_version', 6175) - self.struct.add_version_field('bmu_version', 6178) - self.struct.add_version_field('safety_module_version', 6181) - self.struct.add_version_field('high_voltage_module_version', 6184) - - # DC Solar Input (copied from PR https://github.com/warhammerkid/bluetti_mqtt/pull/87 by KM011092) - self.struct.add_uint_field('pv_input_power1', 1212) # MPP 1 in - value * 0.1 - #self.struct.add_uint_field('pv_input_voltage1', 1213) # MPP 1 in - value * 0.1 - #self.struct.add_uint_field('pv_input_current1', 1214) # MPP 1 in - self.struct.add_uint_field('pv_input_power2', 1220) # MPP 2 in - value * 0.1 - #self.struct.add_uint_field('pv_input_voltage2', 1221) # MPP 2 in - value * 0.1 - #self.struct.add_uint_field('pv_input_current2', 1222) # MPP 2 in - - # 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_phase2", 1236) - self.struct.add_uint_field("adl400_ac_input_power_phase3", 1244) - self.struct.add_decimal_field("adl400_ac_input_voltage_phase1", 1229, 1) - self.struct.add_decimal_field("adl400_ac_input_voltage_phase2", 1237, 1) - self.struct.add_decimal_field("adl400_ac_input_voltage_phase3", 1245, 1) - - # Grid Input - self.struct.add_decimal_field("grid_input_frequency", 1300, 1) - self.struct.add_uint_field("grid_input_power_phase1", 1313) - self.struct.add_uint_field("grid_input_power_phase2", 1319) - self.struct.add_uint_field("grid_input_power_phase3", 1325) - self.struct.add_decimal_field("grid_input_voltage_phase1", 1314, 1) - self.struct.add_decimal_field("grid_input_voltage_phase2", 1320, 1) - self.struct.add_decimal_field("grid_input_voltage_phase3", 1326, 1) - self.struct.add_decimal_field("grid_input_current_phase1", 1315, 1) - self.struct.add_decimal_field("grid_input_current_phase2", 1321, 1) - self.struct.add_decimal_field("grid_input_current_phase3", 1327, 1) - - # EP600 AC Output - self.struct.add_decimal_field("ac_output_frequency", 1500, 1) - self.struct.add_uint_field("ac_output_power_phase1", 1510) - self.struct.add_uint_field("ac_output_power_phase2", 1517) - self.struct.add_uint_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_phase2", 1518, 1) - self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1) - self.struct.add_decimal_field("ac_output_current_phase1", 1512, 1) - self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1) - self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1) - - super().__init__(address, 'EP600', sn) - - @property - def polling_commands(self) -> List[ReadHoldingRegisters]: - return [ - ReadHoldingRegisters(100, 62), - ReadHoldingRegisters(1228, 19), - ReadHoldingRegisters(1300, 28), - ReadHoldingRegisters(1500, 27), - ReadHoldingRegisters(2022, 6), - ReadHoldingRegisters(2213, 4), - ReadHoldingRegisters(1212, 11), - # Battery - ReadHoldingRegisters(6101, 7), - ReadHoldingRegisters(6175, 11), - ] - - @property - def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return [] - - @property - def logging_commands(self) -> List[ReadHoldingRegisters]: - return [ - ReadHoldingRegisters(100, 62), - ReadHoldingRegisters(1100, 51), - ReadHoldingRegisters(1200, 90), - ReadHoldingRegisters(1300, 31), - ReadHoldingRegisters(1400, 48), - ReadHoldingRegisters(1500, 30), - ReadHoldingRegisters(2000, 89), - ReadHoldingRegisters(2200, 41), - ReadHoldingRegisters(2300, 36), - ReadHoldingRegisters(6000, 32), - ReadHoldingRegisters(6100, 100), - ReadHoldingRegisters(6300, 100), - ] diff --git a/custom_components/bluetti_bt/utils.py b/custom_components/bluetti_bt/utils.py index a42f928..2062f12 100644 --- a/custom_components/bluetti_bt/utils.py +++ b/custom_components/bluetti_bt/utils.py @@ -1,11 +1,11 @@ """Tools.""" -from bluetti_mqtt.bluetooth import build_device as orig_build_device +from .bluetti_bt_lib.utils.device_builder import build_device as orig_build_device from .const import DEVICE_NAME_RE -from .devices.ep600 import EP600 from .devices.ep760 import EP760 + def mac_loggable(mac: str) -> str: """Remove parts of the mac address for logging.""" splitted = mac.split(":") @@ -21,7 +21,5 @@ def build_device(address: str, name: str): match = DEVICE_NAME_RE.match(name) if match[1] == 'EP760': return EP760(address, match[2]) - elif match[1] == 'EP600': - return EP600(address, match[2]) else: return orig_build_device(address, name) From 8409221675a69911f4f191a4c91b4d509a986866 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 13:35:17 +0100 Subject: [PATCH 30/82] added ep760 --- custom_components/bluetti_bt/binary_sensor.py | 4 +++- .../bluetti_bt/bluetti_bt_lib/devices/ep760.py | 8 ++++++++ .../bluetti_bt_lib/utils/device_builder.py | 7 ++++++- custom_components/bluetti_bt/coordinator.py | 3 ++- custom_components/bluetti_bt/sensor.py | 4 +++- custom_components/bluetti_bt/switch.py | 4 +++- custom_components/bluetti_bt/utils.py | 13 ------------- 7 files changed, 25 insertions(+), 18 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ep760.py diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index b8e5598..1931b13 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -24,10 +24,12 @@ from bluetti_mqtt.mqtt_client import ( MqttFieldType, ) +from .bluetti_bt_lib.utils.device_builder import build_device + from . import device_info as dev_info, get_unique_id from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator -from .utils import unique_id_loggable, build_device +from .utils import unique_id_loggable _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep760.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep760.py new file mode 100644 index 0000000..b7126f7 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep760.py @@ -0,0 +1,8 @@ +"""EP760 fields.""" + +from ..base_devices.ProtocolV2Device import ProtocolV2Device + + +class EP760(ProtocolV2Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "EP760", sn) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index f549558..3fb036e 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -10,8 +10,11 @@ from ..devices.eb3a import EB3A from ..devices.ep500 import EP500 from ..devices.ep500p import EP500P from ..devices.ep600 import EP600 +from ..devices.ep760 import EP760 -DEVICE_NAME_RE = re.compile(r"^(AC60|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600)(\d+)$") +DEVICE_NAME_RE = re.compile( + r"^(AC60|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760)(\d+)$" +) def build_device(address: str, name: str): @@ -32,3 +35,5 @@ def build_device(address: str, name: str): return EP500P(address, match[2]) if match[1] == "EP600": return EP600(address, match[2]) + if match[1] == "EP760": + return EP760(address, match[2]) diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 40329d5..b52ba01 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -19,9 +19,10 @@ from homeassistant.helpers.update_coordinator import ( from .bluetti_bt_lib.const import NOTIFY_UUID, RESPONSE_TIMEOUT, WRITE_UUID from .bluetti_bt_lib.exceptions import BadConnectionError, ModbusError, ParseError from .bluetti_bt_lib.utils.commands import ReadHoldingRegisters +from .bluetti_bt_lib.utils.device_builder import build_device from .const import DATA_POLLING_RUNNING, DOMAIN -from .utils import mac_loggable, build_device +from .utils import mac_loggable _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 09a81a7..eb8c4cd 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -29,10 +29,12 @@ from bluetti_mqtt.mqtt_client import ( battery_pack_fields, ) +from .bluetti_bt_lib.utils.device_builder import build_device + from . import device_info as dev_info, get_unique_id from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator -from .utils import unique_id_loggable, build_device +from .utils import unique_id_loggable _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 02fd57f..4428ad1 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -31,10 +31,12 @@ from bluetti_mqtt.mqtt_client import ( MqttFieldType, ) +from .bluetti_bt_lib.utils.device_builder import build_device + from . import device_info as dev_info, get_unique_id from .const import CONTROL_FIELDS, DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator -from .utils import mac_loggable, unique_id_loggable, build_device +from .utils import mac_loggable, unique_id_loggable _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/utils.py b/custom_components/bluetti_bt/utils.py index 2062f12..fce973c 100644 --- a/custom_components/bluetti_bt/utils.py +++ b/custom_components/bluetti_bt/utils.py @@ -1,11 +1,5 @@ """Tools.""" -from .bluetti_bt_lib.utils.device_builder import build_device as orig_build_device - -from .const import DEVICE_NAME_RE -from .devices.ep760 import EP760 - - def mac_loggable(mac: str) -> str: """Remove parts of the mac address for logging.""" splitted = mac.split(":") @@ -16,10 +10,3 @@ def unique_id_loggable(unique_id: str) -> str: splitted = unique_id.split("_", maxsplit=1) serial = splitted[0][:6] return serial + "XXXXXXXXXX" + splitted[1] - -def build_device(address: str, name: str): - match = DEVICE_NAME_RE.match(name) - if match[1] == 'EP760': - return EP760(address, match[2]) - else: - return orig_build_device(address, name) From 18825f9062aa05611567b29d41f0555a7144272c Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 13:36:54 +0100 Subject: [PATCH 31/82] removed old ep760 class --- custom_components/bluetti_bt/devices/ep760.py | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 custom_components/bluetti_bt/devices/ep760.py diff --git a/custom_components/bluetti_bt/devices/ep760.py b/custom_components/bluetti_bt/devices/ep760.py deleted file mode 100644 index 6028f23..0000000 --- a/custom_components/bluetti_bt/devices/ep760.py +++ /dev/null @@ -1,67 +0,0 @@ -"""EP760 Field definitions. - -Changes made here are only temporary and should also be -contributed to https://github.com/warhammerkid/bluetti_mqtt -""" - -from typing import List - -from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice -from bluetti_mqtt.core.devices.struct import DeviceStruct -from bluetti_mqtt.core.commands import ReadHoldingRegisters - -class EP760(BluettiDevice): - def __init__(self, address: str, sn: str): - self.struct = DeviceStruct() - - # TODO has to be tested - - self.struct.add_uint_field('total_battery_percent', 102) - self.struct.add_swap_string_field('device_type', 110, 6) - self.struct.add_sn_field('serial_number', 116) - self.struct.add_decimal_field('power_generation', 154, 1) - self.struct.add_swap_string_field('device_type', 1101, 6) - self.struct.add_sn_field('serial_number', 1107) - self.struct.add_decimal_field('power_generation', 1202, 1) - self.struct.add_uint_field('battery_range_start', 2022) - self.struct.add_uint_field('battery_range_end', 2023) - self.struct.add_uint_field('max_ac_input_power', 2213) - self.struct.add_uint_field('max_ac_input_current', 2214) - self.struct.add_uint_field('max_ac_output_power', 2215) - self.struct.add_uint_field('max_ac_output_current', 2216) - self.struct.add_swap_string_field('battery_type', 6101, 6) - self.struct.add_sn_field('battery_serial_number', 6107) - self.struct.add_version_field('bcu_version', 6175) - self.struct.add_version_field('bmu_version', 6178) - self.struct.add_version_field('safety_module_version', 6181) - self.struct.add_version_field('high_voltage_module_version', 6184) - - super().__init__(address, 'EP760', sn) - - @property - def polling_commands(self) -> List[ReadHoldingRegisters]: - return [ - ReadHoldingRegisters(100, 62), - ReadHoldingRegisters(2022, 2), - ] - - @property - def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return [] - - @property - def logging_commands(self) -> List[ReadHoldingRegisters]: - return [ - ReadHoldingRegisters(100, 62), - ReadHoldingRegisters(1100, 51), - ReadHoldingRegisters(1200, 90), - ReadHoldingRegisters(1300, 31), - ReadHoldingRegisters(1400, 48), - ReadHoldingRegisters(1500, 30), - ReadHoldingRegisters(2000, 89), - ReadHoldingRegisters(2200, 41), - ReadHoldingRegisters(2300, 36), - ReadHoldingRegisters(6000, 32), - ReadHoldingRegisters(6100, 100), - ReadHoldingRegisters(6300, 100), - ] From c45994a9a60d981e516103dd5ad5543eef706b38 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 13:54:40 +0100 Subject: [PATCH 32/82] replaced bluetti_mqtt imports with local lib --- custom_components/bluetti_bt/binary_sensor.py | 17 +- .../bluetti_bt_lib/field_attributes.py | 2 +- custom_components/bluetti_bt/const.py | 507 ------------------ custom_components/bluetti_bt/sensor.py | 42 +- custom_components/bluetti_bt/switch.py | 23 +- 5 files changed, 29 insertions(+), 562 deletions(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index 1931b13..53d10d8 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -18,16 +18,11 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) -from bluetti_mqtt.mqtt_client import ( - NORMAL_DEVICE_FIELDS, - DC_INPUT_FIELDS, - MqttFieldType, -) - +from .bluetti_bt_lib.field_attributes import FIELD_ATTRIBUTES, FieldType from .bluetti_bt_lib.utils.device_builder import build_device from . import device_info as dev_info, get_unique_id -from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import PollingCoordinator from .utils import unique_id_loggable @@ -52,12 +47,10 @@ async def async_setup_entry( bluetti_device = build_device(address, device_name) sensors_to_add = [] - all_fields = NORMAL_DEVICE_FIELDS - all_fields.update(DC_INPUT_FIELDS) - all_fields.update(ADDITIONAL_DEVICE_FIELDS) + all_fields = FIELD_ATTRIBUTES for field_key, field_config in all_fields.items(): if bluetti_device.has_field(field_key): - if field_config.type == MqttFieldType.BOOL: + if field_config.type == FieldType.BOOL: category = None if field_config.setter is True: category = EntityCategory.DIAGNOSTIC @@ -68,7 +61,7 @@ async def async_setup_entry( device_info, address, field_key, - field_config.home_assistant_extra.get(CONF_NAME, ""), + field_config.name, category=category, ) ) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 0b9516d..cbb6667 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -79,7 +79,7 @@ class OutletFieldAttributes(FieldAttributes): device_class = "outlet" -FIELD_ATTRIBUTES = { +FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { # Base device fields "dc_input_power": PowerFieldAttributes( name="DC Input Power", diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 7015358..8082c62 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -2,8 +2,6 @@ import re -from bluetti_mqtt.mqtt_client import MqttFieldConfig, MqttFieldType - DOMAIN = "bluetti_bt" MANUFACTURER = "Bluetti" @@ -44,508 +42,3 @@ DIAGNOSTIC_FIELDS = [ "safety_module_version", "high_voltage_module_version", ] - -ADDITIONAL_DEVICE_FIELDS = { - "max_ac_input_power": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Max AC Input Power per Phase", - }, - ), - "max_ac_input_current": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Max AC Input Current per Phase", - }, - ), - "max_ac_output_power": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Max AC Output Power per Phase", - }, - ), - "max_ac_output_current": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Max AC Output Current per Phase", - }, - ), - "bcu_version": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={"name": "BCU Version"}, - ), - "safety_module_version": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={"name": "Safety Module Version"}, - ), - "high_voltage_module_version": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={"name": "High Voltage Safety Module Version"}, - ), - 'pv_input_power1': MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Solar Input Power 1", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - 'pv_input_voltage1': MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Solar Input Voltage 1", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - 'pv_input_current1': MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Solar Input Current 1", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - 'pv_input_power2': MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Solar Input Power 2", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - 'pv_input_voltage2': MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Solar Input Voltage 2", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - 'pv_input_current2': MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Solar Input Current 2", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - "adl400_ac_input_power_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "ADL400 AC Input Power Phase 1", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "adl400_ac_input_power_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "ADL400 AC Input Power Phase 2", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "adl400_ac_input_power_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "ADL400 AC Input Power Phase 3", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "adl400_ac_input_voltage_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "ADL400 AC Input Voltage Phase 1", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "adl400_ac_input_voltage_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "ADL400 AC Input Voltage Phase 2", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "adl400_ac_input_voltage_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "ADL400 AC Input Voltage Phase 3", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_voltage_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Voltage Phase 1", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_voltage_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Voltage Phase 2", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_voltage_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Voltage Phase 3", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_current_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Current Phase 1", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_current_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Current Phase 2", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_current_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Current Phase 3", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_power_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Power Phase 1", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_power_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Power Phase 2", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "grid_input_power_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Grid Input Power Phase 3", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_frequency": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Frequency", - "unit_of_measurement": "Hz", - "device_class": "frequency", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_voltage_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Voltage Phase 1", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_voltage_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Voltage Phase 2", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_voltage_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Voltage Phase 3", - "unit_of_measurement": "V", - "device_class": "voltage", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_power_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Power Phase 1", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_power_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Power Phase 2", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_power_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Power Phase 3", - "unit_of_measurement": "W", - "device_class": "power", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_current_phase1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Current Phase 1", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_current_phase2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Current Phase 2", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - "ac_output_current_phase3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "AC Output Current Phase 3", - "unit_of_measurement": "A", - "device_class": "current", - "state_class": "measurement", - "force_update": True, - }, - ), - "testing0": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 0", - }, - ), - "testing1": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 1", - }, - ), - "testing2": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 2", - }, - ), - "testing3": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 3", - }, - ), - "testing4": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 4", - }, - ), - "testing5": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 5", - }, - ), - "testing6": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 6", - }, - ), - "testing7": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 7", - }, - ), - "testing8": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 8", - }, - ), - "testing9": MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=False, - home_assistant_extra={ - "name": "Register Testing 9", - }, - ), -} diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index eb8c4cd..7aed069 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -6,33 +6,23 @@ from enum import Enum import logging from decimal import Decimal -from homeassistant.components.sensor import SensorEntity, CONF_STATE_CLASS +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.const import ( CONF_ADDRESS, CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, - CONF_DEVICE_CLASS, EntityCategory, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, -) - -from bluetti_mqtt.mqtt_client import ( - NORMAL_DEVICE_FIELDS, - DC_INPUT_FIELDS, - MqttFieldType, - battery_pack_fields, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .bluetti_bt_lib.field_attributes import FIELD_ATTRIBUTES, PACK_FIELD_ATTRIBUTES, FieldType from .bluetti_bt_lib.utils.device_builder import build_device from . import device_info as dev_info, get_unique_id -from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS +from .const import DATA_COORDINATOR, DOMAIN, DIAGNOSTIC_FIELDS from .coordinator import PollingCoordinator from .utils import unique_id_loggable @@ -57,45 +47,43 @@ async def async_setup_entry( bluetti_device = build_device(address, device_name) sensors_to_add = [] - all_fields = NORMAL_DEVICE_FIELDS - all_fields.update(DC_INPUT_FIELDS) - all_fields.update(ADDITIONAL_DEVICE_FIELDS) + all_fields = FIELD_ATTRIBUTES if len(bluetti_device.pack_polling_commands) > 0: # add pack fields for device _LOGGER.info("Device type(%s) pack_num_max(%s)", bluetti_device.type, bluetti_device.pack_num_max) for pack in range (1, bluetti_device.pack_num_max + 1): - for name, field in battery_pack_fields(pack).items(): + for name, field in PACK_FIELD_ATTRIBUTES(pack).items(): all_fields.update({name+str(pack): field}) for field_key, field_config in all_fields.items(): - if bluetti_device.has_field(field_key) or field_config.id_override is not None: + if bluetti_device.has_field(field_key): category = None if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS: category = EntityCategory.DIAGNOSTIC - if field_config.type == MqttFieldType.NUMERIC: + if field_config.type == FieldType.NUMERIC: sensors_to_add.append( BluettiSensor( hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], device_info, address, field_key, - field_config.home_assistant_extra.get(CONF_NAME, ""), - field_config.home_assistant_extra.get(CONF_UNIT_OF_MEASUREMENT), - field_config.home_assistant_extra.get(CONF_DEVICE_CLASS), - field_config.home_assistant_extra.get(CONF_STATE_CLASS), + field_config.name, + field_config.unit_of_measurement, + field_config.device_class, + field_config.state_class, category=category, ) ) - elif field_config.type == MqttFieldType.ENUM: + elif field_config.type == FieldType.ENUM: sensors_to_add.append( BluettiSensor( hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], device_info, address, field_key, - field_config.home_assistant_extra.get(CONF_NAME, ""), - options=field_config.home_assistant_extra.get(CONF_OPTIONS), + field_config.name, + options=[o.value for o in field_config.options], category=category, ) ) diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 4428ad1..f1bca59 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -23,18 +23,13 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, ) -from bluetti_mqtt.bluetooth.client import BluetoothClient -from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice -from bluetti_mqtt.mqtt_client import ( - NORMAL_DEVICE_FIELDS, - DC_INPUT_FIELDS, - MqttFieldType, -) - +from .bluetti_bt_lib.base_devices.BluettiDevice import BluettiDevice +from .bluetti_bt_lib.const import WRITE_UUID +from .bluetti_bt_lib.field_attributes import FIELD_ATTRIBUTES, PACK_FIELD_ATTRIBUTES, FieldType from .bluetti_bt_lib.utils.device_builder import build_device from . import device_info as dev_info, get_unique_id -from .const import CONTROL_FIELDS, DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS +from .const import CONTROL_FIELDS, DATA_COORDINATOR, DOMAIN from .coordinator import PollingCoordinator from .utils import mac_loggable, unique_id_loggable @@ -59,12 +54,10 @@ async def async_setup_entry( bluetti_device = build_device(address, device_name) sensors_to_add = [] - all_fields = NORMAL_DEVICE_FIELDS - all_fields.update(DC_INPUT_FIELDS) - all_fields.update(ADDITIONAL_DEVICE_FIELDS) + all_fields = FIELD_ATTRIBUTES for field_key, field_config in all_fields.items(): if bluetti_device.has_field(field_key): - if field_config.type == MqttFieldType.BOOL: + if field_config.type == FieldType.BOOL: if field_config.setter is True and field_key in CONTROL_FIELDS: sensors_to_add.append( BluettiSwitch( @@ -73,7 +66,7 @@ async def async_setup_entry( device_info, address, field_key, - field_config.home_assistant_extra.get(CONF_NAME, ""), + field_config.name, entry.entry_id ) ) @@ -171,7 +164,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): # Send command _LOGGER.debug("Requesting %s (%s,%s)", command, self._response_key, state) await self._client.write_gatt_char( - BluetoothClient.WRITE_UUID, bytes(command) + WRITE_UUID, bytes(command) ) # Wait until device has changed value, otherwise reading register might reset it From d8999c64702b43db6ecacbe6fddce94dee0d1502 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 14:21:47 +0100 Subject: [PATCH 33/82] fixed field_attributes dataclasses --- .../bluetti_bt_lib/field_attributes.py | 114 ++++++++++++------ custom_components/bluetti_bt/const.py | 3 - 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index cbb6667..d29e4dd 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -22,61 +22,103 @@ class FieldType(Enum): BUTTON = auto() -@dataclass(frozen=True) class FieldAttributes: - type: FieldType - setter: bool = False - name: str - unit_of_measurement: str | None = None - device_class: str | None = None - state_class: str | None = None - options: Enum | None = None + def __init__( + self, + type: FieldType = FieldType.NUMERIC, + setter: bool = False, + name: str = "", + unit_of_measurement: str | None = None, + device_class: str | None = None, + state_class: str | None = None, + options: Enum | None = None, + ): + self.type = type + self.setter = setter + self.name = name + self.unit_of_measurement = unit_of_measurement + self.device_class = device_class + self.state_class = state_class + self.options = options -@dataclass(frozen=True) class PowerFieldAttributes(FieldAttributes): - type = FieldType.NUMERIC - unit_of_measurement = "W" - device_class = "power" - state_class = "measurement" + def __init__( + self, + name: str = "", + ): + super().__init__( + name=name, + unit_of_measurement="W", + device_class="power", + state_class="measurement", + ) -@dataclass class VoltageFieldAttributes(FieldAttributes): - type = FieldType.NUMERIC - unit_of_measurement = "V" - device_class = "voltage" - state_class = "measurement" + def __init__( + self, + name: str = "", + ): + super().__init__( + name=name, + unit_of_measurement="V", + device_class="voltage", + state_class="measurement", + ) -@dataclass class CurrentFieldAttributes(FieldAttributes): - type = FieldType.NUMERIC - unit_of_measurement = "A" - device_class = "current" - state_class = "measurement" + def __init__( + self, + name: str = "", + ): + super().__init__( + name=name, + unit_of_measurement="A", + device_class="current", + state_class="measurement", + ) -@dataclass class EnergyFieldAttributes(FieldAttributes): - type = FieldType.NUMERIC - unit_of_measurement = "kWh" - device_class = "energy" - state_class = "total_increasing" + def __init__( + self, + name: str = "", + ): + super().__init__( + name=name, + unit_of_measurement="kWh", + device_class="energy", + state_class="total_increasing", + ) -@dataclass class FrequencyFieldAttributes(FieldAttributes): - type = (FieldType.NUMERIC,) - unit_of_measurement = ("Hz",) - device_class = ("frequency",) - state_class = ("measurement",) + def __init__( + self, + name: str = "", + ): + super().__init__( + name=name, + unit_of_measurement="Hz", + device_class="frequency", + state_class="measurement", + ) -@dataclass(frozen=True) class OutletFieldAttributes(FieldAttributes): - type = FieldType.BOOL - device_class = "outlet" + def __init__( + self, + name: str = "", + setter: bool = False, + ): + super().__init__( + type=FieldType.BOOL, + name=name, + setter=setter, + device_class="outlet", + ) FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 8082c62..0f6b42d 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -1,7 +1,5 @@ """Constants for the Bluetti BT integration.""" -import re - DOMAIN = "bluetti_bt" MANUFACTURER = "Bluetti" @@ -26,7 +24,6 @@ SUPPORTED_MODELS = [ "EP600", "EP760", ] -DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|EP500P|EP500|EP600|EP760|EB3A)(\d+)$') CONTROL_FIELDS = [ "ac_output_on", From caac9330b4e8944d60615d9a60de21c705d98053 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 14:26:55 +0100 Subject: [PATCH 34/82] field_attributes cleanup --- .../bluetti_bt_lib/field_attributes.py | 206 +++++------------- 1 file changed, 56 insertions(+), 150 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index d29e4dd..1cb046e 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -123,21 +123,11 @@ class OutletFieldAttributes(FieldAttributes): FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { # Base device fields - "dc_input_power": PowerFieldAttributes( - name="DC Input Power", - ), - "ac_input_power": PowerFieldAttributes( - name="AC Input Power", - ), - "ac_output_power": PowerFieldAttributes( - name="AC Output Power", - ), - "dc_output_power": PowerFieldAttributes( - name="DC Output Power", - ), - "power_generation": EnergyFieldAttributes( - name="Total Power Generation", - ), + "dc_input_power": PowerFieldAttributes("DC Input Power"), + "ac_input_power": PowerFieldAttributes("AC Input Power"), + "ac_output_power": PowerFieldAttributes("AC Output Power"), + "dc_output_power": PowerFieldAttributes("DC Output Power"), + "power_generation": EnergyFieldAttributes("Total Power Generation"), "total_battery_percent": FieldAttributes( type=FieldType.NUMERIC, name="Total Battery Percent", @@ -145,15 +135,9 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { device_class="battery", state_class="measurement", ), - "total_battery_voltage": VoltageFieldAttributes( - name="Total Battery Voltage", - ), - "ac_output_on": OutletFieldAttributes( - name="AC Output", - ), - "dc_output_on": OutletFieldAttributes( - name="DC Output", - ), + "total_battery_voltage": VoltageFieldAttributes("Total Battery Voltage"), + "ac_output_on": OutletFieldAttributes("AC Output"), + "dc_output_on": OutletFieldAttributes("DC Output"), # Controls "ac_output_on_switch": OutletFieldAttributes( name="AC Output", @@ -169,150 +153,72 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { name="AC Output Mode", options=OutputMode, ), - "internal_ac_voltage": VoltageFieldAttributes( - name="Internal AC Voltage", - ), - "internal_current_one": CurrentFieldAttributes( - name="Internal Current Sensor 1", - ), - "internal_power_one": PowerFieldAttributes( - name="Internal Power Sensor 1", - ), - "internal_ac_frequency": FrequencyFieldAttributes( - name="Internal AC Frequency", - ), - "internal_current_two": CurrentFieldAttributes( - name="Internal Current Sensor 2", - ), - "internal_power_two": PowerFieldAttributes( - name="Internal Power Sensor 2", - ), - "ac_input_voltage": VoltageFieldAttributes( - name="AC Input Voltage", - ), - "internal_current_three": CurrentFieldAttributes( - name="Internal Current Sensor 3", - ), - "internal_power_three": PowerFieldAttributes( - name="Internal Power Sensor 3", - ), - "ac_input_frequency": FrequencyFieldAttributes( - name="AC Input Frequency", - ), - "internal_dc_input_voltage": VoltageFieldAttributes( - name="Internal DC Input Voltage", - ), - "internal_dc_input_power": PowerFieldAttributes( - name="Internal DC Input Power", - ), - "internal_dc_input_current": CurrentFieldAttributes( - name="Internal DC Input Current", - ), - "pv_input_power1": PowerFieldAttributes( - name="Solar Input Power 1", - ), - "pv_input_power2": PowerFieldAttributes( - name="Solar Input Power 2", - ), - "pv_input_voltage1": VoltageFieldAttributes( - name="Solar Input Voltage 1", - ), - "pv_input_voltage2": VoltageFieldAttributes( - name="Solar Input Voltage 2", - ), - "pv_input_current1": CurrentFieldAttributes( - name="Solar Input Current 1", - ), - "pv_input_current2": CurrentFieldAttributes( - name="Solar Input Current 2", - ), + "internal_ac_voltage": VoltageFieldAttributes("Internal AC Voltage"), + "internal_current_one": CurrentFieldAttributes("Internal Current Sensor 1"), + "internal_power_one": PowerFieldAttributes("Internal Power Sensor 1"), + "internal_ac_frequency": FrequencyFieldAttributes("Internal AC Frequency"), + "internal_current_two": CurrentFieldAttributes("Internal Current Sensor 2"), + "internal_power_two": PowerFieldAttributes("Internal Power Sensor 2"), + "ac_input_voltage": VoltageFieldAttributes("AC Input Voltage"), + "internal_current_three": CurrentFieldAttributes("Internal Current Sensor 3"), + "internal_power_three": PowerFieldAttributes("Internal Power Sensor 3"), + "ac_input_frequency": FrequencyFieldAttributes("AC Input Frequency"), + "internal_dc_input_voltage": VoltageFieldAttributes("Internal DC Input Voltage"), + "internal_dc_input_power": PowerFieldAttributes("Internal DC Input Power"), + "internal_dc_input_current": CurrentFieldAttributes("Internal DC Input Current"), + "pv_input_power1": PowerFieldAttributes("Solar Input Power 1"), + "pv_input_power2": PowerFieldAttributes("Solar Input Power 2"), + "pv_input_voltage1": VoltageFieldAttributes("Solar Input Voltage 1"), + "pv_input_voltage2": VoltageFieldAttributes("Solar Input Voltage 2"), + "pv_input_current1": CurrentFieldAttributes("Solar Input Current 1"), + "pv_input_current2": CurrentFieldAttributes("Solar Input Current 2"), "adl400_ac_input_power_phase1": PowerFieldAttributes( - name="AC Solar Input Power Phase 1" + "AC Solar Input Power Phase 1" ), "adl400_ac_input_power_phase2": PowerFieldAttributes( - name="AC Solar Input Power Phase 2" + "AC Solar Input Power Phase 2" ), "adl400_ac_input_power_phase3": PowerFieldAttributes( - name="AC Solar Input Power Phase 3" + "AC Solar Input Power Phase 3" ), "adl400_ac_input_voltage_phase1": VoltageFieldAttributes( - name="AC Solar Input Voltage Phase 1", + "AC Solar Input Voltage Phase 1" ), "adl400_ac_input_voltage_phase2": VoltageFieldAttributes( - name="AC Solar Input Voltage Phase 2", + "AC Solar Input Voltage Phase 2" ), "adl400_ac_input_voltage_phase3": VoltageFieldAttributes( - name="AC Solar Input Voltage Phase 3", + "AC Solar Input Voltage Phase 3" ), "adl400_ac_input_current_phase1": CurrentFieldAttributes( - name="AC Solar Input Current Phase 1", + "AC Solar Input Current Phase 1" ), "adl400_ac_input_current_phase2": CurrentFieldAttributes( - name="AC Solar Input Current Phase 2", + "AC Solar Input Current Phase 2" ), "adl400_ac_input_current_phase3": CurrentFieldAttributes( - name="AC Solar Input Current Phase 3", - ), - "grid_frequency": FrequencyFieldAttributes( - name="Grid Frequency", - ), - "grid_power_phase1": PowerFieldAttributes( - name="Grid Power Phase 1", - ), - "grid_power_phase2": PowerFieldAttributes( - name="Grid Power Phase 2", - ), - "grid_power_phase3": PowerFieldAttributes( - name="Grid Power Phase 3", - ), - "grid_voltage_phase1": VoltageFieldAttributes( - name="Grid Voltage Phase 1", - ), - "grid_voltage_phase2": VoltageFieldAttributes( - name="Grid Voltage Phase 2", - ), - "grid_voltage_phase3": VoltageFieldAttributes( - name="Grid Voltage Phase 3", - ), - "grid_current_phase1": CurrentFieldAttributes( - name="Grid Current Phase 1", - ), - "grid_current_phase2": CurrentFieldAttributes( - name="Grid Current Phase 2", - ), - "grid_current_phase3": CurrentFieldAttributes( - name="Grid Current Phase 3", - ), - "ac_output_frequency": FrequencyFieldAttributes( - name="AC Output Frequency", - ), - "ac_output_power_phase1": PowerFieldAttributes( - name="AC Output Power Phase 1", - ), - "ac_output_power_phase2": PowerFieldAttributes( - name="AC Output Power Phase 2", - ), - "ac_output_power_phase3": PowerFieldAttributes( - name="AC Output Power Phase 3", - ), - "ac_output_voltage_phase1": VoltageFieldAttributes( - name="AC Output Voltage Phase 1", - ), - "ac_output_voltage_phase2": VoltageFieldAttributes( - name="AC Output Voltage Phase 2", - ), - "ac_output_voltage_phase3": VoltageFieldAttributes( - name="AC Output Voltage Phase 3", - ), - "total_ac_consumption": EnergyFieldAttributes( - name="Total Load Consumption", - ), - "total_grid_consumption": EnergyFieldAttributes( - name="Total Grid Consumption", - ), - "total_grid_feed": EnergyFieldAttributes( - name="Total Grid Feed", + "AC Solar Input Current Phase 3" ), + "grid_frequency": FrequencyFieldAttributes("Grid Frequency"), + "grid_power_phase1": PowerFieldAttributes("Grid Power Phase 1"), + "grid_power_phase2": PowerFieldAttributes("Grid Power Phase 2"), + "grid_power_phase3": PowerFieldAttributes("Grid Power Phase 3"), + "grid_voltage_phase1": VoltageFieldAttributes("Grid Voltage Phase 1"), + "grid_voltage_phase2": VoltageFieldAttributes("Grid Voltage Phase 2"), + "grid_voltage_phase3": VoltageFieldAttributes("Grid Voltage Phase 3"), + "grid_current_phase1": CurrentFieldAttributes("Grid Current Phase 1"), + "grid_current_phase2": CurrentFieldAttributes("Grid Current Phase 2"), + "grid_current_phase3": CurrentFieldAttributes("Grid Current Phase 3"), + "ac_output_frequency": FrequencyFieldAttributes("AC Output Frequency"), + "ac_output_power_phase1": PowerFieldAttributes("AC Output Power Phase 1"), + "ac_output_power_phase2": PowerFieldAttributes("AC Output Power Phase 2"), + "ac_output_power_phase3": PowerFieldAttributes("AC Output Power Phase 3"), + "ac_output_voltage_phase1": VoltageFieldAttributes("AC Output Voltage Phase 1"), + "ac_output_voltage_phase2": VoltageFieldAttributes("AC Output Voltage Phase 2"), + "ac_output_voltage_phase3": VoltageFieldAttributes("AC Output Voltage Phase 3"), + "total_ac_consumption": EnergyFieldAttributes("Total Load Consumption"), + "total_grid_consumption": EnergyFieldAttributes("Total Grid Consumption"), + "total_grid_feed": EnergyFieldAttributes("Total Grid Feed"), # Device specific controls "power_off": FieldAttributes( type=FieldType.BUTTON, From 564a971d7ca87c3f0d0e6c57e6591c8ec9631ed2 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 14:35:16 +0100 Subject: [PATCH 35/82] move get_type_by_bt_name into lib --- custom_components/bluetti_bt/__init__.py | 22 ------------------- .../bluetti_bt_lib/field_attributes.py | 1 - .../bluetti_bt_lib/utils/device_builder.py | 7 ++++++ custom_components/bluetti_bt/config_flow.py | 4 +++- 4 files changed, 10 insertions(+), 24 deletions(-) diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 46be53c..ea7e4aa 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -84,25 +84,3 @@ def get_unique_id(name: str, sensor_type: str | None = None): if sensor_type is not None: return f"{sensor_type}.{res}" return res - - -def get_type_by_bt_name(bt_name: str): - """Get the device type.""" - dev_type = "Unknown" - if bt_name.startswith("AC200M"): - dev_type = "AC200M" - elif bt_name.startswith("AC300"): - dev_type = "AC300" - elif bt_name.startswith("AC500"): - dev_type = "AC500" - elif bt_name.startswith("AC60"): - dev_type = "AC60" - elif bt_name.startswith("EB3A"): - dev_type = "EB3A" - elif bt_name.startswith("EP500"): - dev_type = "EP500" - elif bt_name.startswith("EP600"): - dev_type = "EP600" - elif bt_name.startswith("EP760"): - dev_type = "EP760" - return dev_type diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 1cb046e..5cec159 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -1,6 +1,5 @@ """Field attributes for device fields.""" -from dataclasses import dataclass from enum import Enum, auto, unique from .field_enums import ( diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index 3fb036e..177610d 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -37,3 +37,10 @@ def build_device(address: str, name: str): return EP600(address, match[2]) if match[1] == "EP760": return EP760(address, match[2]) + + +def get_type_by_bt_name(bt_name: str): + match = DEVICE_NAME_RE.match(bt_name) + if len(match) > 0: + return match[1] + return "Unknown" diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 9ebd5b8..6b2899e 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import re import logging +from typing import Any import voluptuous as vol @@ -16,7 +17,8 @@ from homeassistant.const import CONF_ADDRESS, CONF_TYPE, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector -from . import get_type_by_bt_name +from .bluetti_bt_lib.utils.device_builder import get_type_by_bt_name + from .const import ( CONF_MAX_RETRIES, CONF_PERSISTENT_CONN, From 38e1fa4ecdf79ff4928513910b4c4454c6f9dcdd Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 14:44:44 +0100 Subject: [PATCH 36/82] added ReadHoldingRegisters for ep600 --- .../bluetti_bt/bluetti_bt_lib/devices/ep600.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py index 8c9a808..047ce97 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py @@ -88,6 +88,22 @@ class EP600(ProtocolV2Device): return super().polling_commands + [ ReadHoldingRegisters(2022, 2), ReadHoldingRegisters(2213, 5), + ReadHoldingRegisters(1212, 3), + ReadHoldingRegisters(1220, 3), + ReadHoldingRegisters(1228, 3), + ReadHoldingRegisters(1236, 3), + ReadHoldingRegisters(1244, 3), + ReadHoldingRegisters(1300, 1), + ReadHoldingRegisters(1313, 3), + ReadHoldingRegisters(1319, 3), + ReadHoldingRegisters(1325, 3), + ReadHoldingRegisters(1500, 1), + ReadHoldingRegisters(1510, 3), + ReadHoldingRegisters(1517, 3), + ReadHoldingRegisters(1524, 3), + ReadHoldingRegisters(152, 1), + ReadHoldingRegisters(156, 1), + ReadHoldingRegisters(158, 1), ] # @property From e40e056e5681888fdbe42f9e7a8a19b8b3942cfa Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 14:46:52 +0100 Subject: [PATCH 37/82] added more diagnostic fields --- custom_components/bluetti_bt/const.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 0f6b42d..e6246f1 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -38,4 +38,8 @@ DIAGNOSTIC_FIELDS = [ "bcu_version", "safety_module_version", "high_voltage_module_version", + "power_generation", + "total_ac_consumption", + "total_grid_consumption", + "total_grid_feed", ] From 9b7fd82591980bae0cc83742aa0bf8ab28e6ddc9 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 16:32:05 +0100 Subject: [PATCH 38/82] extracted DeviceReader class from coordinator --- .../bluetti_bt_lib/bluetooth/device_reader.py | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py new file mode 100644 index 0000000..ffd309d --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -0,0 +1,219 @@ +"""Device reader.""" + +import asyncio +import logging +from typing import Any, Callable, List, cast +import async_timeout +from bleak import BleakClient, BleakError + +from ..base_devices.BluettiDevice import BluettiDevice +from ..const import NOTIFY_UUID, RESPONSE_TIMEOUT, WRITE_UUID +from ..exceptions import BadConnectionError, ModbusError, ParseError +from ..utils.commands import ReadHoldingRegisters + +_LOGGER = logging.getLogger(__name__) + + +class DeviceReader: + def __init__( + self, + bleak_client: BleakClient, + bluetti_device: BluettiDevice, + future_builder_method: Callable[[], asyncio.Future[Any]], + persistent_conn: bool = False, + polling_timeout: int = 45, + max_retries: int = 5, + ) -> None: + self.client = bleak_client + self.bluetti_device = bluetti_device + self.create_future = future_builder_method + self.persistent_conn = persistent_conn + self.polling_timeout = polling_timeout + self.max_retries = max_retries + + self.has_notifier = False + self.notify_future: asyncio.Future[Any] | None = None + self.current_command = None + self.notify_response = bytearray() + + # polling mutex to guard against switches + self.polling_lock = asyncio.Lock() + + async def read_data( + self, filter_registers: List[ReadHoldingRegisters] | None = None + ) -> dict | None: + _LOGGER.info("Reading data") + + if self.bluetti_device is None: + _LOGGER.error("Device is None") + return None + + polling_commands = self.bluetti_device.polling_commands + pack_commands = self.bluetti_device.pack_polling_commands + if filter_registers is not None: + polling_commands = filter_registers + pack_commands = [] + + parsed_data: dict = {} + + async with self.polling_lock: + try: + async with async_timeout.timeout(self.polling_timeout): + # Reconnect if not connected + for attempt in range(1, self.max_retries + 1): + try: + if not self.client.is_connected: + await self.client.connect() + break + except Exception as e: + if attempt == self.max_retries or attempt == 1: + raise # pass exception on max_retries attempt + else: + _LOGGER.warning( + f"Connect unsucessful (attempt {attempt}): {e}. Retrying..." + ) + await asyncio.sleep(2) + + # Attach notifier if needed + if not self.has_notifier: + await self.client.start_notify( + NOTIFY_UUID, self._notification_handler + ) + self.has_notifier = True + + # Execute polling commands + for command in polling_commands: + try: + body = command.parse_response( + await self._async_send_command(command) + ) + _LOGGER.debug("Raw data: %s", body) + parsed = self.bluetti_device.parse( + command.starting_address, body + ) + _LOGGER.debug("Parsed data: %s", parsed) + parsed_data.update(parsed) + except ParseError: + _LOGGER.warning("Got a parse exception") + + # Execute pack polling commands + if len(pack_commands) > 0: + _LOGGER.debug("Polling battery packs") + for pack in range(1, self.bluetti_device.pack_num_max + 1): + # Set current pack number + await self._async_send_command( + self.bluetti_device.build_setter_command( + "pack_num", pack + ) + ) + + # We need to wait after switching packs for the data to be available + await asyncio.sleep(5) + + for command in pack_commands: + # Request & parse result for each pack + try: + body = command.parse_response( + await self._async_send_command(command) + ) + parsed = self.bluetti_device.parse( + command.starting_address, body + ) + _LOGGER.debug("Parsed data: %s", parsed) + + pack_number = parsed.get("pack_num") + if ( + not isinstance(pack_number, int) + or pack_number != pack + ): + _LOGGER.debug( + "Parsed pack_num(%s) does not match expected '%s'", + pack_number, + pack, + ) + continue + + for key, value in parsed.items(): + parsed_data.update({key + str(pack): value}) + + except ParseError: + _LOGGER.warning("Got a parse exception...") + + except TimeoutError: + _LOGGER.error("Polling timed out") + return None + except BleakError as err: + _LOGGER.error("Bleak error: %s", err) + return None + finally: + # Disconnect if connection not persistant + if not self.persistent_conn: + if self.has_notifier: + await self.client.stop_notify(NOTIFY_UUID) + self.has_notifier = False + await self.client.disconnect() + + return parsed_data + + async def _async_send_command(self, command: ReadHoldingRegisters) -> bytes: + """Send command and return response""" + try: + # Prepare to make request + self.current_command = command + self.notify_future = self.create_future() + self.notify_response = bytearray() + + # Make request + _LOGGER.debug("Requesting %s", command) + await self.client.write_gatt_char(WRITE_UUID, bytes(command)) + + # Wait for response + res = await asyncio.wait_for(self.notify_future, timeout=RESPONSE_TIMEOUT) + + # Process data + _LOGGER.debug("Got %s bytes", len(res)) + return cast(bytes, res) + + except TimeoutError: + _LOGGER.warning("Polling single command timed out") + except ModbusError as err: + _LOGGER.warning( + "Got an invalid request error for %s: %s", + command, + err, + ) + except (BadConnectionError, BleakError) as err: + _LOGGER.warning( + "Needed to disconnect due to error: %s (This can also be the case if you used device controls)", + err, + ) + + # caught an exception, return empty bytes object + return bytes() + + def _notification_handler(self, _sender: int, data: bytearray): + """Handle bt data.""" + + # Ignore notifications we don't expect + if self.notify_future is None or self.notify_future.done(): + _LOGGER.warning("Unexpected notification") + return + + # If something went wrong, we might get weird data. + if data == b"AT+NAME?\r" or data == b"AT+ADV?\r": + err = BadConnectionError("Got AT+ notification") + self.notify_future.set_exception(err) + return + + # Save data + self.notify_response.extend(data) + + if len(self.notify_response) == self.current_command.response_size(): + if self.current_command.is_valid_response(self.notify_response): + self.notify_future.set_result(self.notify_response) + else: + self.notify_future.set_exception(ParseError("Failed checksum")) + elif self.current_command.is_exception_response(self.notify_response): + # We got a MODBUS command exception + msg = f"MODBUS Exception {self.current_command}: {self.notify_response[2]}" + self.notify_future.set_exception(ModbusError(msg)) From a50deaae376b69552b5dcc0c81cc7e3f193b087f Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 16:37:37 +0100 Subject: [PATCH 39/82] added device type detection to config_flow --- .../base_devices/ProtocolV2Device.py | 2 +- .../bluetooth/device_recognizer.py | 59 +++++++++++++++++++ .../bluetti_bt_lib/utils/device_builder.py | 6 +- custom_components/bluetti_bt/config_flow.py | 25 ++++---- 4 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_recognizer.py diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py index 92f4808..b383c6c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV2Device.py @@ -12,7 +12,7 @@ class ProtocolV2Device(BluettiDevice): self.struct = DeviceStruct() # Device info - self.struct.add_string_field("device_type", 110, 6) + self.struct.add_swap_string_field("device_type", 110, 6) self.struct.add_sn_field("serial_number", 116) # Power IO diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_recognizer.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_recognizer.py new file mode 100644 index 0000000..dc5b426 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_recognizer.py @@ -0,0 +1,59 @@ +"""Device recognizer.""" + +import asyncio +import logging +from typing import Any, Callable +from bleak import BleakClient + +from custom_components.bluetti_bt.bluetti_bt_lib.utils.commands import ( + ReadHoldingRegisters, +) + +from ..base_devices.ProtocolV2Device import ProtocolV2Device +from ..bluetooth.device_reader import DeviceReader + +_LOGGER = logging.getLogger(__name__) + + +async def recognize_device( + bleak_client: BleakClient, + future_builder_method: Callable[[], asyncio.Future[Any]], +) -> str: + + # Since we don't know the type we use the base device + bluetti_device = ProtocolV2Device("Unknown", "Unknown", "Unknown") + + # Create device builder + device_reader = DeviceReader(bleak_client, bluetti_device, future_builder_method) + + # Retry a few times to get data + for _ in range(1, 50): + # We only need 6 registers to get the device type + data = await device_reader.read_data( + [ + ReadHoldingRegisters(110, 6), + ] + ) + + if data is not None: + break + + await asyncio.sleep(0.1) + + if data is None: + # Should not happen + return "Unknown" + + field_data = data.get("device_type") + + if field_data is None: + # We have a problem + _LOGGER.error("No data in device type field_datas. Maybe it's a V1 device?") + return "Unknown" + + if not isinstance(field_data, str): + # We have a problem + _LOGGER.error("Invalid data in device type field_datas") + return "Unknown" + + return field_data diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index 177610d..1d3856d 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -41,6 +41,6 @@ def build_device(address: str, name: str): def get_type_by_bt_name(bt_name: str): match = DEVICE_NAME_RE.match(bt_name) - if len(match) > 0: - return match[1] - return "Unknown" + if match is None or len(match) < 2: + return "Unknown" + return match[1] diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 6b2899e..0d20d13 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -5,6 +5,7 @@ from __future__ import annotations import re import logging from typing import Any +from bleak import BleakClient import voluptuous as vol @@ -13,11 +14,12 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.const import CONF_ADDRESS, CONF_TYPE, CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .bluetti_bt_lib.utils.device_builder import get_type_by_bt_name +from .bluetti_bt_lib.bluetooth.device_recognizer import recognize_device from .const import ( CONF_MAX_RETRIES, @@ -26,7 +28,6 @@ from .const import ( CONF_POLLING_TIMEOUT, CONF_USE_CONTROLS, DOMAIN, - SUPPORTED_MODELS, ) _LOGGER = logging.getLogger(__name__) @@ -46,6 +47,14 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle bluetooth discovery.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() + + # Get device type if needed + if isinstance(discovery_info.name, str) and discovery_info.name.startswith("PBOX"): + bleak_device = BleakClient(discovery_info.device) + device_type = await recognize_device(bleak_device, self.hass.loop.create_future) + _LOGGER.info("Device identified as %s", device_type) + discovery_info.name = discovery_info.name.replace("PBOX", device_type.strip()) + self._discovery_info = discovery_info self.context["title_placeholders"] = {"name": discovery_info.name} return await self.async_step_user() @@ -60,18 +69,12 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info = self._discovered_devices[address] await self.async_set_unique_id(address, raise_on_progress=False) self._abort_if_unique_id_configured() - dev_type = get_type_by_bt_name(discovery_info.name) name = re.sub("[^A-Z0-9]+", "", discovery_info.name) - if dev_type == "Unknown": - dev_type = user_input[CONF_TYPE] - name = name.replace("PBOX", dev_type) - return self.async_create_entry( title=name, data={ CONF_ADDRESS: discovery_info.address, - CONF_TYPE: dev_type, CONF_NAME: name, }, ) @@ -97,12 +100,6 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for service_info in self._discovered_devices.values() } ), - vol.Optional(CONF_TYPE): vol.In( - { - model: model - for model in SUPPORTED_MODELS - } - ), } ) return self.async_show_form( From 842f133cd424014fe2e40c89d771ddf1ade1df78 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 16:49:27 +0100 Subject: [PATCH 40/82] code cleanup --- custom_components/bluetti_bt/binary_sensor.py | 2 +- custom_components/bluetti_bt/coordinator.py | 201 ++---------------- custom_components/bluetti_bt/sensor.py | 2 +- custom_components/bluetti_bt/switch.py | 2 +- 4 files changed, 16 insertions(+), 191 deletions(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index 53d10d8..7737c63 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -99,7 +99,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - if self.coordinator.persistent_conn and not self.coordinator.client.is_connected: + if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected: return _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index b52ba01..c85b4c8 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -1,14 +1,11 @@ """Coordinator for Bluetti integration.""" from __future__ import annotations -from typing import cast -import asyncio from datetime import timedelta import logging -import async_timeout -from bleak import BleakClient, BleakError +from bleak import BleakClient from homeassistant.components import bluetooth from homeassistant.core import HomeAssistant @@ -16,12 +13,9 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .bluetti_bt_lib.const import NOTIFY_UUID, RESPONSE_TIMEOUT, WRITE_UUID -from .bluetti_bt_lib.exceptions import BadConnectionError, ModbusError, ParseError -from .bluetti_bt_lib.utils.commands import ReadHoldingRegisters +from .bluetti_bt_lib.bluetooth.device_reader import DeviceReader from .bluetti_bt_lib.utils.device_builder import build_device -from .const import DATA_POLLING_RUNNING, DOMAIN from .utils import mac_loggable _LOGGER = logging.getLogger(__name__) @@ -47,15 +41,6 @@ class PollingCoordinator(DataUpdateCoordinator): name="Bluetti polling coordinator", update_interval=timedelta(seconds=polling_interval), ) - self._address = address - self.has_notifier = False - self.notify_future = None - self.current_command = None - self.notify_response = bytearray() - self.bluetti_device = build_device(address, device_name) - self.persistent_conn = persistent_conn - self.polling_timeout = polling_timeout - self.max_retries = max_retries # Create client self.logger.debug("Creating client") @@ -63,10 +48,17 @@ class PollingCoordinator(DataUpdateCoordinator): if device is None: self.logger.error("Device %s not available", mac_loggable(address)) return None - self.client = BleakClient(device) + client = BleakClient(device) + bluetti_device = build_device(address, device_name) - # polling mutex to guard against switches - self.polling_lock = asyncio.Lock() + self.reader = DeviceReader( + client, + bluetti_device, + self.hass.loop.create_future, + persistent_conn=persistent_conn, + polling_timeout=polling_timeout, + max_retries=max_retries, + ) async def _async_update_data(self): """Fetch data from API endpoint. @@ -74,172 +66,5 @@ class PollingCoordinator(DataUpdateCoordinator): This is the place to pre-process the data to lookup tables so entities can quickly look up their data. """ - self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = True - self.logger.debug("Polling data") - - if self.bluetti_device is None: - self.logger.error("Device type for %s not found", mac_loggable(self._address)) - return None - - parsed_data: dict = {} - - async with self.polling_lock: - try: - async with async_timeout.timeout(self.polling_timeout): - - # Reconnect if not connected - for attempt in range(1, self.max_retries + 1): - try: - if not self.client.is_connected: - await self.client.connect() - break - except Exception as e: - if attempt == self.max_retries or attempt == 1: - raise # pass exception on max_retries attempt - else: - self.logger.warning(f"Connect unsucessful (attempt {attempt}): {e}. Retrying...") - await asyncio.sleep(2) - - # Attach notifier if needed - if not self.has_notifier: - await self.client.start_notify( - NOTIFY_UUID, self._notification_handler - ) - self.has_notifier = True - - for command in self.bluetti_device.polling_commands: - try: - body = command.parse_response( - await self.async_send_command(command) - ) - self.logger.debug("Raw data: %s", body) - parsed = self.bluetti_device.parse( - command.starting_address, body - ) - self.logger.debug("Parsed data: %s", parsed) - parsed_data.update(parsed) - - except ParseError: - self.logger.warning("Got a parse exception...") - - if len(self.bluetti_device.pack_polling_commands) > 0: - self.logger.debug("Polling battery packs") - # pack polling - for pack in range (1, self.bluetti_device.pack_num_max + 1): - # Set current pack number - await self.async_send_command( - self.bluetti_device.build_setter_command('pack_num', pack) - ) - - # We need to wait after switching packs for the data to be available - await asyncio.sleep(5) - - for command in self.bluetti_device.pack_polling_commands: - # Request & parse result for each pack - try: - body = command.parse_response( - await self.async_send_command(command) - ) - parsed = self.bluetti_device.parse( - command.starting_address, body - ) - self.logger.debug("Parsed data: %s", parsed) - - pack_number = parsed.get('pack_num') - if not isinstance(pack_number, int) or pack_number != pack: - self.logger.debug("Parsed pack_num(%s) does not match expected '%s'", pack_number, pack) - continue - - for key, value in parsed.items(): - parsed_data.update({key+str(pack):value}) - - except ParseError: - self.logger.warning("Got a parse exception...") - - except TimeoutError: - self.logger.warning("Polling timed out for device %s", mac_loggable(self._address)) - return None - except BleakError as err: - self.logger.warning("Bleak error: %s", err) - return None - finally: - # Disconnect if connection not persistant - if not self.persistent_conn: - if self.has_notifier: - await self.client.stop_notify(NOTIFY_UUID) - self.has_notifier = False - await self.client.disconnect() - - self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = False - - # Pass data back to sensors - return parsed_data - - async def async_send_command(self, command: ReadHoldingRegisters) -> bytes: - """Send command and return response""" - try: - # Prepare to make request - self.current_command = command - self.notify_future = self.hass.loop.create_future() - self.notify_response = bytearray() - - # Make request - self.logger.debug("Requesting %s", command) - await self.client.write_gatt_char( - WRITE_UUID, bytes(command) - ) - - # Wait for response - res = await asyncio.wait_for( - self.notify_future, timeout=RESPONSE_TIMEOUT - ) - - # Process data - self.logger.debug("Got %s bytes", len(res)) - return cast(bytes, res) - - except TimeoutError: - self.logger.debug( - "Polling timed out (address: %s)", mac_loggable(self._address) - ) - except ModbusError as err: - self.logger.warning( - "Got an invalid request error for %s: %s", - command, - err, - ) - except (BadConnectionError, BleakError) as err: - self.logger.warning( - "Needed to disconnect due to error: %s (This can also be the case if you used device controls)", err - ) - - # caught an exception, return empty bytes object - return bytes() - - def _notification_handler(self, _sender: int, data: bytearray): - """Handle bt data.""" - - # Ignore notifications we don't expect - if not self.notify_future or self.notify_future.done(): - _LOGGER.warning("Unexpected notification") - return - - # If something went wrong, we might get weird data. - if data == b"AT+NAME?\r" or data == b"AT+ADV?\r": - err = BadConnectionError("Got AT+ notification") - self.notify_future.set_exception(err) - return - - # Save data - self.notify_response.extend(data) - - if len(self.notify_response) == self.current_command.response_size(): - if self.current_command.is_valid_response(self.notify_response): - self.notify_future.set_result(self.notify_response) - else: - self.notify_future.set_exception(ParseError("Failed checksum")) - elif self.current_command.is_exception_response(self.notify_response): - # We got a MODBUS command exception - msg = f"MODBUS Exception {self.current_command}: {self.notify_response[2]}" - self.notify_future.set_exception(ModbusError(msg)) + return await self.reader.read_data() diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 7aed069..d024a38 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -129,7 +129,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - if self.coordinator.persistent_conn and not self.coordinator.client.is_connected: + if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected: return if self.coordinator.data is None: diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index f1bca59..c1231f1 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -112,7 +112,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - if self.coordinator.persistent_conn and not self.coordinator.client.is_connected: + if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected: return _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) From c59881de7fa41bd1cc2ef581795b5457c9c07518 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 16:52:23 +0100 Subject: [PATCH 41/82] fix switch coordinator references --- custom_components/bluetti_bt/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index c1231f1..4481fdf 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -93,8 +93,8 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): self._bluetti_device = bluetti_device self._coordinator = coordinator - self._client = coordinator.client - self._polling_lock = coordinator.polling_lock + self._client = coordinator.reader.client + self._polling_lock = coordinator.reader.polling_lock e_name = f"{device_info.get('name')} {name}" self._address = address self._response_key = response_key @@ -178,7 +178,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): return None finally: # Disconnect if connection not persistant - if not self._coordinator.persistent_conn: + if not self._coordinator.reader.persistent_conn: await self._client.disconnect() await self.coordinator.async_request_refresh() From 3f51dd829e1e6cdbf11f1837f307689ec7e772f5 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 18 Feb 2024 17:02:24 +0100 Subject: [PATCH 42/82] prepare beta version 0.0.17 --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index ff05ddb..b4cb9ab 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -20,5 +20,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.16" + "version": "0.0.17" } From 6412630c85594b51e86a07ac0b9b2121b97234ad Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 25 Feb 2024 15:03:27 +0100 Subject: [PATCH 43/82] added new int field --- .../bluetti_bt/bluetti_bt_lib/utils/struct.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index ea9418f..0e74706 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -44,6 +44,21 @@ class UintField(DeviceField): 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): def __init__(self, name: str, address: int): 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): 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): self.fields.append(BoolField(name, address)) From 4d27f108faf960ecf20ffd8080fffaf12fc9bccb Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 25 Feb 2024 15:04:18 +0100 Subject: [PATCH 44/82] fixed negative ac and dc solar values --- .../bluetti_bt/bluetti_bt_lib/devices/ep600.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py index 047ce97..bfbc861 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py @@ -20,11 +20,11 @@ class EP600(ProtocolV2Device): # DC Solar self.struct.add_uint_field("pv_input_power1", 1212) - self.struct.add_uint_field("pv_input_voltage1", 1213) - self.struct.add_uint_field("pv_input_current1", 1214) + self.struct.add_decimal_field("pv_input_voltage1", 1213, 1) + 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_voltage2", 1221) - self.struct.add_uint_field("pv_input_current2", 1222) + self.struct.add_decimal_field("pv_input_voltage2", 1221, 1) + self.struct.add_decimal_field("pv_input_current2", 1222, 1) # ADL400 Smart Meter for AC Solar self.struct.add_uint_field("adl400_ac_input_power_phase1", 1228) @@ -52,9 +52,9 @@ class EP600(ProtocolV2Device): # EP600 AC Output self.struct.add_decimal_field("ac_output_frequency", 1500, 1) - self.struct.add_uint_field("ac_output_power_phase1", 1510) - self.struct.add_uint_field("ac_output_power_phase2", 1517) - self.struct.add_uint_field("ac_output_power_phase3", 1524) + self.struct.add_int_field("ac_output_power_phase1", 1510) + self.struct.add_int_field("ac_output_power_phase2", 1517) + 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_phase2", 1518, 1) self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1) From 9cba466228547e79e95f1cd118b450d99e3d7de7 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 25 Feb 2024 15:37:12 +0100 Subject: [PATCH 45/82] basic ep800 support --- README.md | 3 ++- .../bluetti_bt/bluetti_bt_lib/devices/ep800.py | 8 ++++++++ .../bluetti_bt/bluetti_bt_lib/utils/device_builder.py | 5 ++++- custom_components/bluetti_bt/manifest.json | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ep800.py diff --git a/README.md b/README.md index b0a50be..e12c034 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ After the installation, you can use this button to install the integration: - EP500 - EP500P - EP600 (tested) -- EP760 (beta, untested) +- EP760 (basic data) +- EP800 (basic data) ### Available controls: If enabled in the Integration options (you need to reload the integration if you change this option): diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep800.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep800.py new file mode 100644 index 0000000..7ca5dc1 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep800.py @@ -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) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index 1d3856d..a79b5ca 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -11,9 +11,10 @@ from ..devices.ep500 import EP500 from ..devices.ep500p import EP500P from ..devices.ep600 import EP600 from ..devices.ep760 import EP760 +from ..devices.ep800 import EP800 DEVICE_NAME_RE = re.compile( - r"^(AC60|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760)(\d+)$" + r"^(AC60|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" ) @@ -37,6 +38,8 @@ def build_device(address: str, name: str): return EP600(address, match[2]) if match[1] == "EP760": return EP760(address, match[2]) + if match[1] == "EP800": + return EP800(address, match[2]) def get_type_by_bt_name(bt_name: str): diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index b4cb9ab..0024fb4 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -10,6 +10,7 @@ { "local_name": "EP500*" }, { "local_name": "EP600*" }, { "local_name": "EP760*" }, + { "local_name": "EP800*" }, { "local_name": "PBOX*" } ], "codeowners": ["@Patrick762"], From 5fa1c8631febfb43fc62a036de37703707d89963 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 25 Feb 2024 15:37:48 +0100 Subject: [PATCH 46/82] next beta release --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 0024fb4..c4e42bb 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -21,5 +21,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.17" + "version": "0.0.18" } From da5443172bac423a8eff9f350fdf865a2312f6e4 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 25 Feb 2024 16:41:29 +0100 Subject: [PATCH 47/82] added ac180 and ac180p --- README.md | 2 ++ .../bluetti_bt/bluetti_bt_lib/devices/ac180.py | 8 ++++++++ .../bluetti_bt/bluetti_bt_lib/devices/ac180p.py | 8 ++++++++ .../bluetti_bt/bluetti_bt_lib/utils/device_builder.py | 8 +++++++- custom_components/bluetti_bt/manifest.json | 3 ++- 5 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180.py create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180p.py diff --git a/README.md b/README.md index e12c034..f865ea6 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ After the installation, you can use this button to install the integration: ### Supported devices: - AC60 +- AC180 (basic data) +- AC180P (basic data) - AC200M - AC300 (tested) - AC500 diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180.py new file mode 100644 index 0000000..565e3f8 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180.py @@ -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) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180p.py new file mode 100644 index 0000000..eeb4236 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac180p.py @@ -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) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index a79b5ca..c113422 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -3,6 +3,8 @@ import re from ..devices.ac60 import AC60 +from ..devices.ac180 import AC180 +from ..devices.ac180p import AC180P from ..devices.ac200m import AC200M from ..devices.ac300 import AC300 from ..devices.ac500 import AC500 @@ -14,7 +16,7 @@ from ..devices.ep760 import EP760 from ..devices.ep800 import EP800 DEVICE_NAME_RE = re.compile( - r"^(AC60|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" + r"^(AC60|AC180|AC180P|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" ) @@ -22,6 +24,10 @@ def build_device(address: str, name: str): match = DEVICE_NAME_RE.match(name) if match[1] == "AC60": 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": return AC200M(address, match[2]) if match[1] == "AC300": diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index c4e42bb..1d5205e 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -2,10 +2,11 @@ "domain": "bluetti_bt", "name": "Bluetti BT", "bluetooth": [ + { "local_name": "AC60*" }, + { "local_name": "AC180*" }, { "local_name": "AC200M*" }, { "local_name": "AC300*" }, { "local_name": "AC500*" }, - { "local_name": "AC60*" }, { "local_name": "EB3A*" }, { "local_name": "EP500*" }, { "local_name": "EP600*" }, From 8901c11afcc49b518d802a132c9c801d27da6ebf Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 25 Feb 2024 16:41:47 +0100 Subject: [PATCH 48/82] added unittest --- test.sh | 4 ++ tests/device_builder_test.py | 124 +++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100755 test.sh create mode 100644 tests/device_builder_test.py diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..90b73db --- /dev/null +++ b/test.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +# Execute unittests of python library +python3 -m unittest discover -s tests -p "*_test.py" diff --git a/tests/device_builder_test.py b/tests/device_builder_test.py new file mode 100644 index 0000000..705596d --- /dev/null +++ b/tests/device_builder_test.py @@ -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() From df8375016c72175bba9ec92ceb37604bcedbf9ed Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sun, 25 Feb 2024 16:46:14 +0100 Subject: [PATCH 49/82] prepare next beta release --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 1d5205e..bf82827 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -22,5 +22,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.18" + "version": "0.0.19" } From ec996d5728408b0cd319be334cba1abfc3eb689c Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Tue, 12 Mar 2024 20:12:39 +0100 Subject: [PATCH 50/82] added debug logging to init --- custom_components/bluetti_bt/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index ea7e4aa..8d68e05 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -33,6 +33,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Bluetti Powerstation from a config entry.""" + _LOGGER.debug("Init Bluetti BT Integration") + address = entry.data.get(CONF_ADDRESS) device_name = entry.data.get(CONF_NAME) 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) # Create coordinator for polling + _LOGGER.debug("Creating coordinator") coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistent_conn, polling_timeout, max_retries) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator) + _LOGGER.debug("Creating entities") platforms: list = PLATFORMS if use_controls is True: _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 await hass.config_entries.async_forward_entry_setups(entry, platforms) + _LOGGER.debug("Setup done") + return True From c499fbcd9be5b1d2d7fd3b985abf2acb47914ab2 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Tue, 12 Mar 2024 20:13:20 +0100 Subject: [PATCH 51/82] fix writable_ranges --- custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py | 2 +- custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py | 2 +- custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py | 4 ++-- custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py | 2 +- custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 5151c32..3ceae5c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -58,7 +58,7 @@ class AC300(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3016), - 3061, + range(3061), ] @property diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 12edcb2..ca0dcc9 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -57,7 +57,7 @@ class AC500(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3016), - 3061, + range(3061), ] @property diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py index 6f37a66..7e26356 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py @@ -33,7 +33,7 @@ class EB3A(ProtocolV1Device): @property def writable_ranges(self) -> List[range]: return super().writable_ranges + [ - 3034, - 3060, + range(3034), + range(3060), range(3063, 3066), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index 1c5ff4a..c95c60e 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -53,7 +53,7 @@ class EP500(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3016), - 3061, + range(3061), ] @property diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index f70d906..af462cb 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -53,7 +53,7 @@ class EP500P(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3016), - 3061, + range(3061), ] @property From 7596f8cee1163e0646078ab2e5fd94ad93bdb76e Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Tue, 12 Mar 2024 20:25:24 +0100 Subject: [PATCH 52/82] fix ac_output_on switches --- custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py | 2 ++ custom_components/bluetti_bt/const.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py index 7e26356..d9376d5 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py @@ -28,6 +28,8 @@ class EB3A(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(77, 1), ReadHoldingRegisters(86, 1), + ReadHoldingRegisters(3060, 1), + ReadHoldingRegisters(3063, 4), ] @property diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index e6246f1..945c793 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -26,8 +26,8 @@ SUPPORTED_MODELS = [ ] CONTROL_FIELDS = [ - "ac_output_on", - "dc_output_on", + "ac_output_on_switch", + "dc_output_on_switch", ] DIAGNOSTIC_FIELDS = [ From babdd1755e16f306f0228fc7bed3510542d99fda Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Tue, 12 Mar 2024 20:55:09 +0100 Subject: [PATCH 53/82] fix dc switch, added some switches for eb3a --- .../bluetti_bt_lib/base_devices/ProtocolV1Device.py | 2 +- .../bluetti_bt/bluetti_bt_lib/devices/eb3a.py | 9 +++++---- .../bluetti_bt/bluetti_bt_lib/field_attributes.py | 5 ++--- custom_components/bluetti_bt/const.py | 3 +++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index b7d717d..6dd4dd0 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -64,7 +64,7 @@ class ProtocolV1Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: - return [range(3006, 3008)] + return [range(3006, 3009)] @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py index d9376d5..644ac26 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/eb3a.py @@ -28,14 +28,15 @@ class EB3A(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(77, 1), ReadHoldingRegisters(86, 1), + ReadHoldingRegisters(3034, 1), ReadHoldingRegisters(3060, 1), - ReadHoldingRegisters(3063, 4), + ReadHoldingRegisters(3063, 5), ] @property def writable_ranges(self) -> List[range]: return super().writable_ranges + [ - range(3034), - range(3060), - range(3063, 3066), + range(3034, 3035), + range(3060, 3061), + range(3063, 3067), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index 5cec159..cac844c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -18,7 +18,6 @@ class FieldType(Enum): NUMERIC = auto() BOOL = auto() ENUM = auto() - BUTTON = auto() class FieldAttributes: @@ -220,7 +219,7 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { "total_grid_feed": EnergyFieldAttributes("Total Grid Feed"), # Device specific controls "power_off": FieldAttributes( - type=FieldType.BUTTON, + type=FieldType.BOOL, setter=True, name="Power Off", ), @@ -294,7 +293,7 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { ), "power_lifting_on": FieldAttributes( type=FieldType.BOOL, - setter=False, # Disabled for safety reasons + setter=True, name="Power Lifting", ), } diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 945c793..1568e73 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -28,6 +28,9 @@ SUPPORTED_MODELS = [ CONTROL_FIELDS = [ "ac_output_on_switch", "dc_output_on_switch", + "power_off", + "eco_on", + "power_lifting_on", ] DIAGNOSTIC_FIELDS = [ From bf86fe172bd89b1384707c277cec0ae703f723d2 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Tue, 12 Mar 2024 20:56:13 +0100 Subject: [PATCH 54/82] update version number --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index bf82827..f261fe9 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -22,5 +22,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.19" + "version": "0.0.20" } From eb701de8650fc4c081a34f3cdcadc1467ae88814 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 16 Mar 2024 16:57:25 +0100 Subject: [PATCH 55/82] fixed some polling addresses --- .../base_devices/ProtocolV1Device.py | 4 +--- .../bluetti_bt/bluetti_bt_lib/devices/ac200m.py | 9 +++++---- .../bluetti_bt/bluetti_bt_lib/devices/ac300.py | 14 ++++++++++---- .../bluetti_bt/bluetti_bt_lib/devices/ac500.py | 14 ++++++++++---- .../bluetti_bt/bluetti_bt_lib/devices/ep500.py | 14 ++++++++++---- .../bluetti_bt/bluetti_bt_lib/devices/ep500p.py | 14 ++++++++++---- 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index 6dd4dd0..ff66723 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -56,10 +56,8 @@ class ProtocolV1Device(BluettiDevice): ReadHoldingRegisters(36, 4), ReadHoldingRegisters(41, 1), ReadHoldingRegisters(43, 1), - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), ReadHoldingRegisters(48, 2), + ReadHoldingRegisters(3007, 2), ] @property diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index f5fa41a..5c4f4ae 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -13,10 +13,10 @@ class AC200M(ProtocolV1Device): # Details self.struct.add_enum_field("ac_output_mode", 70, OutputMode) - self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_ac_voltage", 71, 1) self.struct.add_decimal_field("internal_current_one", 72, 1) self.struct.add_uint_field("internal_power_one", 73) - self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_uint_field("internal_dc_input_voltage", 86) self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) @@ -35,13 +35,14 @@ class AC200M(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: return super().polling_commands + [ - ReadHoldingRegisters(70, 4), + ReadHoldingRegisters(70, 5), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(3060, 2), ] @property def writable_ranges(self) -> List[range]: - return super().writable_ranges + [range(3060, 3061)] + return super().writable_ranges + [range(3060, 3062)] @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 3ceae5c..5a73e4a 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -13,10 +13,10 @@ class AC300(ProtocolV1Device): # Details (matches EP500 + AC500) self.struct.add_enum_field("ac_output_mode", 70, OutputMode) - self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_ac_voltage", 71, 1) self.struct.add_decimal_field("internal_current_one", 72, 1) self.struct.add_uint_field("internal_power_one", 73) - self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_decimal_field("internal_current_two", 75, 1) self.struct.add_uint_field("internal_power_two", 76) self.struct.add_decimal_field("ac_input_voltage", 77, 1) @@ -50,14 +50,20 @@ class AC300(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: return super().polling_commands + [ - ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(3001, 1), + ReadHoldingRegisters(3004, 2), + ReadHoldingRegisters(3011, 1), + ReadHoldingRegisters(3013, 1), + ReadHoldingRegisters(3015, 2), + ReadHoldingRegisters(3061, 1), ] @property def writable_ranges(self) -> List[range]: return super().writable_ranges + [ - range(3001, 3016), + range(3001, 3017), range(3061), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index ca0dcc9..099ee6f 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -13,10 +13,10 @@ class AC500(ProtocolV1Device): # Details (matches EP500 + AC300) self.struct.add_enum_field("ac_output_mode", 70, OutputMode) - self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_ac_voltage", 71, 1) self.struct.add_decimal_field("internal_current_one", 72, 1) self.struct.add_uint_field("internal_power_one", 73) - self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_decimal_field("internal_current_two", 75, 1) self.struct.add_uint_field("internal_power_two", 76) self.struct.add_decimal_field("ac_input_voltage", 77, 1) @@ -49,14 +49,20 @@ class AC500(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: return super().polling_commands + [ - ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(3001, 1), + ReadHoldingRegisters(3004, 2), + ReadHoldingRegisters(3011, 1), + ReadHoldingRegisters(3013, 1), + ReadHoldingRegisters(3015, 2), + ReadHoldingRegisters(3061, 1), ] @property def writable_ranges(self) -> List[range]: return super().writable_ranges + [ - range(3001, 3016), + range(3001, 3017), range(3061), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index c95c60e..184cbc9 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -13,10 +13,10 @@ class EP500(ProtocolV1Device): # Details (matches AC500 + AC300) self.struct.add_enum_field("ac_output_mode", 70, OutputMode) - self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_ac_voltage", 71, 1) self.struct.add_decimal_field("internal_current_one", 72, 1) self.struct.add_uint_field("internal_power_one", 73) - self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_decimal_field("internal_current_two", 75, 1) self.struct.add_uint_field("internal_power_two", 76) self.struct.add_decimal_field("ac_input_voltage", 77, 1) @@ -45,14 +45,20 @@ class EP500(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: return super().polling_commands + [ - ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(3001, 1), + ReadHoldingRegisters(3004, 2), + ReadHoldingRegisters(3011, 1), + ReadHoldingRegisters(3013, 1), + ReadHoldingRegisters(3015, 2), + ReadHoldingRegisters(3061, 1), ] @property def writable_ranges(self) -> List[range]: return super().writable_ranges + [ - range(3001, 3016), + range(3001, 3017), range(3061), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index af462cb..53df796 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -13,10 +13,10 @@ class EP500P(ProtocolV1Device): # Details (matches AC500 + AC300) self.struct.add_enum_field("ac_output_mode", 70, OutputMode) - self.struct.add_uint_field("internal_ac_voltage", 71) + self.struct.add_decimal_field("internal_ac_voltage", 71, 1) self.struct.add_decimal_field("internal_current_one", 72, 1) self.struct.add_uint_field("internal_power_one", 73) - self.struct.add_decimal_field("internal_ac_frequency", 74, 1) + self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_decimal_field("internal_current_two", 75, 1) self.struct.add_uint_field("internal_power_two", 76) self.struct.add_decimal_field("ac_input_voltage", 77, 1) @@ -45,14 +45,20 @@ class EP500P(ProtocolV1Device): @property def polling_commands(self) -> List[ReadHoldingRegisters]: return super().polling_commands + [ - ReadHoldingRegisters(70, 10), + ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(3001, 1), + ReadHoldingRegisters(3004, 2), + ReadHoldingRegisters(3011, 1), + ReadHoldingRegisters(3013, 1), + ReadHoldingRegisters(3015, 2), + ReadHoldingRegisters(3061, 1), ] @property def writable_ranges(self) -> List[range]: return super().writable_ranges + [ - range(3001, 3016), + range(3001, 3017), range(3061), ] From 542f244a282eb3952f932044dae45f5659d645aa Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 16 Mar 2024 17:27:28 +0100 Subject: [PATCH 56/82] removed pack polling from base device --- .../base_devices/ProtocolV1Device.py | 15 --------------- .../bluetti_bt/bluetti_bt_lib/devices/ac200m.py | 12 +++++++++++- .../bluetti_bt/bluetti_bt_lib/devices/ac300.py | 14 +++++++++++--- .../bluetti_bt/bluetti_bt_lib/devices/ac500.py | 16 ++++++++++++---- .../bluetti_bt/bluetti_bt_lib/devices/ep500.py | 16 ++++++++++++---- .../bluetti_bt/bluetti_bt_lib/devices/ep500p.py | 16 ++++++++++++---- .../bluetti_bt/bluetti_bt_lib/devices/ep600.py | 11 ----------- custom_components/bluetti_bt/sensor.py | 2 ++ 8 files changed, 60 insertions(+), 42 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index ff66723..7f1f585 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -29,13 +29,6 @@ class ProtocolV1Device(BluettiDevice): # Battery self.struct.add_uint_field("total_battery_percent", 43) - # Battery packs - self.struct.add_uint_field("pack_num_max", 91) # internal - self.struct.add_decimal_field("total_battery_voltage", 92, 1) - self.struct.add_uint_field("pack_num", 96) # internal - self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage - self.struct.add_uint_field("pack_battery_percent", 99) - # Output state self.struct.add_bool_field("ac_output_on", 48) self.struct.add_bool_field("dc_output_on", 49) @@ -63,11 +56,3 @@ class ProtocolV1Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: return [range(3006, 3009)] - - @property - def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return [ - ReadHoldingRegisters(91, 2), - ReadHoldingRegisters(96, 1), - ReadHoldingRegisters(98, 2), - ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index 5c4f4ae..79feaea 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -22,6 +22,11 @@ class AC200M(ProtocolV1Device): self.struct.add_decimal_field("internal_dc_input_current", 88, 2) # Battery packs + self.struct.add_uint_field("pack_num_max", 91) # internal + self.struct.add_decimal_field("total_battery_voltage", 92, 1) + self.struct.add_uint_field("pack_num", 96) # internal + self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage + self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal # Controls @@ -46,4 +51,9 @@ class AC200M(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return super().pack_polling_commands + [ReadHoldingRegisters(105, 16)] + return [ + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), + ReadHoldingRegisters(105, 16) + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 5a73e4a..b94bd5f 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -11,7 +11,7 @@ class AC300(ProtocolV1Device): def __init__(self, address: str, sn: str): super().__init__(address, "AC300", sn) - # Details (matches EP500 + AC500) + # Details 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_current_one", 72, 1) @@ -28,10 +28,15 @@ class AC300(ProtocolV1Device): self.struct.add_decimal_field("internal_dc_input_current", 88, 2) # Battery packs + self.struct.add_uint_field("pack_num_max", 91) # internal + self.struct.add_decimal_field("total_battery_voltage", 92, 1) + self.struct.add_uint_field("pack_num", 96) # internal + self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage + self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_version_field("pack_bms_version", 201) - # Controls (matches EP500 + AC500) + # Controls self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_enum_field( @@ -69,7 +74,10 @@ class AC300(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return super().pack_polling_commands + [ + return [ + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), ReadHoldingRegisters(105, 16), ReadHoldingRegisters(201, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 099ee6f..e9b6832 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -11,7 +11,7 @@ class AC500(ProtocolV1Device): def __init__(self, address: str, sn: str): super().__init__(address, "AC500", sn) - # Details (matches EP500 + AC300) + # Details 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_current_one", 72, 1) @@ -27,10 +27,15 @@ class AC500(ProtocolV1Device): self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) - # Battery packs (matches EP500) + # Battery packs + self.struct.add_uint_field("pack_num_max", 91) # internal + self.struct.add_decimal_field("total_battery_voltage", 92, 1) + self.struct.add_uint_field("pack_num", 96) # internal + self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage + self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal - # Controls (matches EP500 + AC300) + # Controls self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_enum_field( @@ -68,6 +73,9 @@ class AC500(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return super().pack_polling_commands + [ + return [ + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), ReadHoldingRegisters(105, 16), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index 184cbc9..0f374c6 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -11,7 +11,7 @@ class EP500(ProtocolV1Device): def __init__(self, address: str, sn: str): super().__init__(address, "EP500", sn) - # Details (matches AC500 + AC300) + # Details 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_current_one", 72, 1) @@ -27,10 +27,15 @@ class EP500(ProtocolV1Device): self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) - # Battery packs (matches AC500) + # Battery packs + self.struct.add_uint_field("pack_num_max", 91) # internal + self.struct.add_decimal_field("total_battery_voltage", 92, 1) + self.struct.add_uint_field("pack_num", 96) # internal + self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage + self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal - # Controls (matches AC500 + AC300) + # Controls self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_enum_field( @@ -64,6 +69,9 @@ class EP500(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return super().pack_polling_commands + [ + return [ + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), ReadHoldingRegisters(105, 16), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index 53df796..872874c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -11,7 +11,7 @@ class EP500P(ProtocolV1Device): def __init__(self, address: str, sn: str): super().__init__(address, "EP500P", sn) - # Details (matches AC500 + AC300) + # Details 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_current_one", 72, 1) @@ -27,10 +27,15 @@ class EP500P(ProtocolV1Device): self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) - # Battery packs (matches AC500) + # Battery packs + self.struct.add_uint_field("pack_num_max", 91) # internal + self.struct.add_decimal_field("total_battery_voltage", 92, 1) + self.struct.add_uint_field("pack_num", 96) # internal + self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage + self.struct.add_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal - # Controls (matches AC500 + AC300) + # Controls self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_enum_field( @@ -64,6 +69,9 @@ class EP500P(ProtocolV1Device): @property def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - return super().pack_polling_commands + [ + return [ + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), ReadHoldingRegisters(105, 16), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py index bfbc861..c59485b 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py @@ -79,10 +79,6 @@ class EP600(ProtocolV2Device): self.struct.add_version_field("safety_module_version", 6181) # internal self.struct.add_version_field("high_voltage_module_version", 6184) # internal - # @property - # def pack_num_max(self): - # return 16 - @property def polling_commands(self) -> List[ReadHoldingRegisters]: return super().polling_commands + [ @@ -105,10 +101,3 @@ class EP600(ProtocolV2Device): ReadHoldingRegisters(156, 1), ReadHoldingRegisters(158, 1), ] - - # @property - # def pack_polling_commands(self) -> List[ReadHoldingRegisters]: - # return super().pack_polling_commands + [ - # ReadHoldingRegisters(6101, 10), - # ReadHoldingRegisters(6175, 12), - # ] diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index d024a38..cba296b 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -171,6 +171,8 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): self._attr_available = True + _LOGGER.error(response_data) + # Different for enum and numeric if ( self._options is not None From 15a890fb7d57bf6199fd106779b518ca6106bb96 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 16 Mar 2024 17:49:17 +0100 Subject: [PATCH 57/82] fix enum entities --- custom_components/bluetti_bt/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index cba296b..9390b8e 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -171,16 +171,13 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): self._attr_available = True - _LOGGER.error(response_data) - # Different for enum and numeric if ( self._options is not None and isinstance(response_data, Enum) - and response_data.value < len(self._options) ): # Enum - self._attr_native_value = self._options[response_data.value] + self._attr_native_value = response_data.name else: # Numeric self._attr_native_value = response_data From 95b1dafeb1606a75fd21a4fab0cfd65c80f6682c Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 16 Mar 2024 17:51:46 +0100 Subject: [PATCH 58/82] increase version number --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index f261fe9..0ed9d77 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -22,5 +22,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.20" + "version": "0.0.21" } From 89928e2d9c549080d458f58bb0f2a2969c1010ea Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Mon, 1 Apr 2024 12:29:01 +0200 Subject: [PATCH 59/82] fix dependency for bt adapters --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 0ed9d77..34342ea 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -16,7 +16,7 @@ ], "codeowners": ["@Patrick762"], "config_flow": true, - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "documentation": "https://github.com/Patrick762/hassio-bluetti-bt", "integration_type": "device", "iot_class": "local_polling", From dcfebb0f846815add95c4995c26d160869ae8afc Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Mon, 1 Apr 2024 12:37:22 +0200 Subject: [PATCH 60/82] added untested AC200L support based on AC200M --- README.md | 1 + .../bluetti_bt_lib/devices/ac200l.py | 59 +++++++++++++++++++ .../bluetti_bt_lib/utils/device_builder.py | 5 +- custom_components/bluetti_bt/manifest.json | 1 + 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py diff --git a/README.md b/README.md index f865ea6..1807142 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ After the installation, you can use this button to install the integration: - AC60 - AC180 (basic data) - AC180P (basic data) +- AC200L (untested) - AC200M - AC300 (tested) - AC500 diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py new file mode 100644 index 0000000..ab80a10 --- /dev/null +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py @@ -0,0 +1,59 @@ +"""AC200L fields.""" + +from typing import List + +from ..utils.commands import ReadHoldingRegisters +from ..field_enums import AutoSleepMode, OutputMode +from ..base_devices.ProtocolV1Device import ProtocolV1Device + + +class AC200L(ProtocolV1Device): + def __init__(self, address: str, sn: str): + super().__init__(address, "AC200L", sn) + + # Details + 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_current_one", 72, 1) + self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_decimal_field("internal_ac_frequency", 74, 2) + self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_decimal_field("internal_dc_input_power", 87, 1) + self.struct.add_decimal_field("internal_dc_input_current", 88, 2) + + # Battery packs + self.struct.add_uint_field("pack_num_max", 91) # internal + self.struct.add_decimal_field("total_battery_voltage", 92, 1) + self.struct.add_uint_field("pack_num", 96) # internal + self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage + self.struct.add_uint_field("pack_battery_percent", 99) + self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal + + # Controls + self.struct.add_bool_field("power_off", 3060) + self.struct.add_enum_field("auto_sleep_mode", 3061, AutoSleepMode) + + @property + def pack_num_max(self): + return 3 + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return super().polling_commands + [ + ReadHoldingRegisters(70, 5), + ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(3060, 2), + ] + + @property + def writable_ranges(self) -> List[range]: + return super().writable_ranges + [range(3060, 3062)] + + @property + def pack_polling_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(91, 2), + ReadHoldingRegisters(96, 1), + ReadHoldingRegisters(98, 2), + ReadHoldingRegisters(105, 16) + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index c113422..e0e56c7 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -5,6 +5,7 @@ import re from ..devices.ac60 import AC60 from ..devices.ac180 import AC180 from ..devices.ac180p import AC180P +from ..devices.ac200l import AC200L from ..devices.ac200m import AC200M from ..devices.ac300 import AC300 from ..devices.ac500 import AC500 @@ -16,7 +17,7 @@ from ..devices.ep760 import EP760 from ..devices.ep800 import EP800 DEVICE_NAME_RE = re.compile( - r"^(AC60|AC180|AC180P|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" + r"^(AC60|AC180|AC180P|AC200L|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" ) @@ -28,6 +29,8 @@ def build_device(address: str, name: str): return AC180(address, match[2]) if match[1] == "AC180P": return AC180P(address, match[2]) + if match[1] == "AC200L": + return AC200L(address, match[2]) if match[1] == "AC200M": return AC200M(address, match[2]) if match[1] == "AC300": diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 34342ea..c11b1bf 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -4,6 +4,7 @@ "bluetooth": [ { "local_name": "AC60*" }, { "local_name": "AC180*" }, + { "local_name": "AC200L*" }, { "local_name": "AC200M*" }, { "local_name": "AC300*" }, { "local_name": "AC500*" }, From 2a4c3e943ca284143b9e568eb9bd5057169ea478 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Mon, 1 Apr 2024 12:37:48 +0200 Subject: [PATCH 61/82] updated manifest version --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index c11b1bf..1b826a0 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.21" + "version": "0.0.22" } From a7967e74ddfa4acf5a66ee1b2c8dcd5a9c034db5 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Mon, 1 Apr 2024 14:08:42 +0200 Subject: [PATCH 62/82] fix writeable_ranges for ac500, ac300, ep500 and ep500p --- custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py | 2 +- custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py | 2 +- custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py | 2 +- custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index b94bd5f..7a9cc8f 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -69,7 +69,7 @@ class AC300(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3017), - range(3061), + range(3061, 3062), ] @property diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index e9b6832..2def5c0 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -68,7 +68,7 @@ class AC500(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3017), - range(3061), + range(3061, 3062), ] @property diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index 0f374c6..59101d6 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -64,7 +64,7 @@ class EP500(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3017), - range(3061), + range(3061, 3062), ] @property diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index 872874c..62b62de 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -64,7 +64,7 @@ class EP500P(ProtocolV1Device): def writable_ranges(self) -> List[range]: return super().writable_ranges + [ range(3001, 3017), - range(3061), + range(3061, 3062), ] @property From a93b80796a468d73eecd8122473c67d657fad76a Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Mon, 1 Apr 2024 14:11:24 +0200 Subject: [PATCH 63/82] increase version number --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 1b826a0..1934a2c 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.22" + "version": "0.0.23" } From 5ac8d8d7ae07022eae0c02873ddc7765ba9ff48e Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Mon, 1 Apr 2024 14:31:03 +0200 Subject: [PATCH 64/82] only check pack_num once --- .../bluetti_bt_lib/bluetooth/device_reader.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py index ffd309d..c8cc38b 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -110,6 +110,8 @@ class DeviceReader: # We need to wait after switching packs for the data to be available await asyncio.sleep(5) + pack_ok = False + for command in pack_commands: # Request & parse result for each pack try: @@ -121,17 +123,19 @@ class DeviceReader: ) _LOGGER.debug("Parsed data: %s", parsed) - pack_number = parsed.get("pack_num") - if ( - not isinstance(pack_number, int) - or pack_number != pack - ): - _LOGGER.debug( - "Parsed pack_num(%s) does not match expected '%s'", - pack_number, - pack, - ) - continue + if not pack_ok: # Only check this once + pack_number = parsed.get("pack_num") + if ( + not isinstance(pack_number, int) + or pack_number != pack + ): + _LOGGER.debug( + "Parsed pack_num(%s) does not match expected '%s'", + pack_number, + pack, + ) + continue + pack_ok = True for key, value in parsed.items(): parsed_data.update({key + str(pack): value}) From e584d320370432a5e292be1f1032ac74ba5e0007 Mon Sep 17 00:00:00 2001 From: KM011092 Date: Fri, 19 Apr 2024 11:41:43 +0200 Subject: [PATCH 65/82] Additional EP600 values Added Consumption and Grid, Consumption and Solar in Totals --- .idea/.gitignore | 3 +++ .idea/hassio-bluetti-bt-KM1.iml | 12 +++++++++++ .../inspectionProfiles/profiles_settings.xml | 6 ++++++ .idea/misc.xml | 7 +++++++ .idea/modules.xml | 8 +++++++ .idea/vcs.xml | 6 ++++++ .../bluetti_bt_lib/devices/ep600.py | 21 +++++++++++++++++++ .../bluetti_bt_lib/field_attributes.py | 20 ++++++++++++++++++ 8 files changed, 83 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/hassio-bluetti-bt-KM1.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/hassio-bluetti-bt-KM1.iml b/.idea/hassio-bluetti-bt-KM1.iml new file mode 100644 index 0000000..8b8c395 --- /dev/null +++ b/.idea/hassio-bluetti-bt-KM1.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a6218fe --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a13d29a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py index c59485b..282d1f4 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep600.py @@ -62,6 +62,22 @@ class EP600(ProtocolV2Device): self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1) self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1) + # KM - Consumption + self.struct.add_int_field("consumption_power_phase1", 1430) + self.struct.add_int_field("consumption_power_phase2", 1436) + self.struct.add_int_field("consumption_power_phase3", 1442) + self.struct.add_decimal_field("consumption_voltage_phase1", 1431, 1) + self.struct.add_decimal_field("consumption_voltage_phase2", 1437, 1) + self.struct.add_decimal_field("consumption_voltage_phase3", 1443, 1) + self.struct.add_decimal_field("consumption_current_phase1", 1432, 1) + self.struct.add_decimal_field("consumption_current_phase2", 1438, 1) + self.struct.add_decimal_field("consumption_current_phase3", 1444, 1) + + # KM - Totals + self.struct.add_uint_field('pv_input_power_all', 144) # Total PV in + self.struct.add_uint_field('consumption_power_all', 142) # Total AC out + self.struct.add_uint_field('grid_power_all', 146) # Total Grid in - value only +/- unknown + # Statistics self.struct.add_decimal_field( "total_ac_consumption", 152, 1 @@ -100,4 +116,9 @@ class EP600(ProtocolV2Device): ReadHoldingRegisters(152, 1), ReadHoldingRegisters(156, 1), ReadHoldingRegisters(158, 1), + # KM - Consumption + ReadHoldingRegisters(1430, 15), + # KM - Totals + ReadHoldingRegisters(142, 5), + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py index cac844c..db9707e 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/field_attributes.py @@ -217,6 +217,26 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = { "total_ac_consumption": EnergyFieldAttributes("Total Load Consumption"), "total_grid_consumption": EnergyFieldAttributes("Total Grid Consumption"), "total_grid_feed": EnergyFieldAttributes("Total Grid Feed"), + + # KM - Consumption + "consumption_power_phase1": PowerFieldAttributes("Consumption Power Phase 1"), + "consumption_power_phase2": PowerFieldAttributes("Consumption Power Phase 2"), + "consumption_power_phase3": PowerFieldAttributes("Consumption Power Phase 3"), + "consumption_voltage_phase1": VoltageFieldAttributes("Consumption Voltage Phase 1"), + "consumption_voltage_phase2": VoltageFieldAttributes("Consumption Voltage Phase 2"), + "consumption_voltage_phase3": VoltageFieldAttributes("Consumption Voltage Phase 3"), + "consumption_current_phase1": CurrentFieldAttributes("Consumption Current Phase 1"), + "consumption_current_phase2": CurrentFieldAttributes("Consumption Current Phase 2"), + "consumption_current_phase3": CurrentFieldAttributes("Consumption Current Phase 3"), + # KM - Consumption end + + # KM - Totals + "pv_input_power_all": PowerFieldAttributes("PV Input Power All"), + "consumption_power_all": PowerFieldAttributes("Consumption Power All"), + "grid_power_all": PowerFieldAttributes("Grid Power All"), + # KM - Totals end + + # Device specific controls "power_off": FieldAttributes( type=FieldType.BOOL, From 2c6df77078e391f33c5713a43f3a05f3af01110e Mon Sep 17 00:00:00 2001 From: KM011092 Date: Sun, 28 Apr 2024 17:31:03 +0200 Subject: [PATCH 66/82] Deleted .idea directory --- .idea/.gitignore | 3 --- .idea/hassio-bluetti-bt-KM1.iml | 12 ------------ .idea/inspectionProfiles/profiles_settings.xml | 6 ------ .idea/misc.xml | 7 ------- .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 6 files changed, 42 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/hassio-bluetti-bt-KM1.iml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/hassio-bluetti-bt-KM1.iml b/.idea/hassio-bluetti-bt-KM1.iml deleted file mode 100644 index 8b8c395..0000000 --- a/.idea/hassio-bluetti-bt-KM1.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index a6218fe..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index a13d29a..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From efaa9d0fdb2d232f242f322b481f4903a592dfed Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 12:29:53 +0200 Subject: [PATCH 67/82] updated bug report template --- .github/ISSUE_TEMPLATE/bug_report.yaml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index b1196a2..b84889f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -26,20 +26,10 @@ body: default: 0 validations: required: true - - type: dropdown + - type: textarea id: device attributes: label: What device are you seeing the problem on? - multiple: true - options: - - AC60 - - AC200M - - AC300 - - AC500 - - EB3A - - EP500 - - EP500P - - EP900 validations: required: true - type: dropdown @@ -50,6 +40,7 @@ body: options: - USB dongle - ESPHome bluetooth proxy + - Internal bluetooth adapter validations: required: true - type: dropdown From f5b8e09fb4093ed5be1a1f4338be4a8311c2aa35 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 15:03:47 +0200 Subject: [PATCH 68/82] prevent binary_sensor if controls enabled --- custom_components/bluetti_bt/binary_sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index 7737c63..ab235d4 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -22,7 +22,7 @@ from .bluetti_bt_lib.field_attributes import FIELD_ATTRIBUTES, FieldType from .bluetti_bt_lib.utils.device_builder import build_device from . import device_info as dev_info, get_unique_id -from .const import DATA_COORDINATOR, DOMAIN +from .const import DATA_COORDINATOR, DOMAIN, CONF_USE_CONTROLS from .coordinator import PollingCoordinator from .utils import unique_id_loggable @@ -36,6 +36,7 @@ async def async_setup_entry( device_name = entry.data.get(CONF_NAME) address = entry.data.get(CONF_ADDRESS) + use_controls = entry.data.get(CONF_USE_CONTROLS) if address is None: _LOGGER.error("Device has no address") @@ -55,6 +56,10 @@ async def async_setup_entry( if field_config.setter is True: category = EntityCategory.DIAGNOSTIC + # Ignore setters if controls enabled + if field_config.setter is True and use_controls is True: + continue + sensors_to_add.append( BluettiBinarySensor( hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], From b2672ec30290c15c4aeaeec1113783db7f91802a Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 19:56:23 +0200 Subject: [PATCH 69/82] fix availability states --- custom_components/bluetti_bt/binary_sensor.py | 8 ++++++++ .../bluetti_bt_lib/bluetooth/device_reader.py | 18 ++++++++++++------ custom_components/bluetti_bt/coordinator.py | 8 ++++++++ custom_components/bluetti_bt/sensor.py | 15 ++++++++++++--- custom_components/bluetti_bt/switch.py | 10 +++++++++- 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index ab235d4..ca6457a 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -100,6 +100,11 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): self._attr_unique_id = get_unique_id(e_name) self._attr_entity_category = category + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._attr_available + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -113,11 +118,13 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): "Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False + self.async_write_ha_state() return response_data = self.coordinator.data.get(self._response_key) if response_data is None: self._attr_available = False + self.async_write_ha_state() return if not isinstance(response_data, bool): @@ -127,6 +134,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): response_data, ) self._attr_available = False + self.async_write_ha_state() return self._attr_available = True diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py index c8cc38b..0e04564 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -144,7 +144,7 @@ class DeviceReader: _LOGGER.warning("Got a parse exception...") except TimeoutError: - _LOGGER.error("Polling timed out") + _LOGGER.warning("Polling timed out. Trying again later") return None except BleakError as err: _LOGGER.error("Bleak error: %s", err) @@ -153,10 +153,18 @@ class DeviceReader: # Disconnect if connection not persistant if not self.persistent_conn: if self.has_notifier: - await self.client.stop_notify(NOTIFY_UUID) + try: + await self.client.stop_notify(NOTIFY_UUID) + except: + # Ignore errors here + pass self.has_notifier = False await self.client.disconnect() + # Check if dict is empty + if not parsed_data: + return None + return parsed_data async def _async_send_command(self, command: ReadHoldingRegisters) -> bytes: @@ -187,10 +195,8 @@ class DeviceReader: err, ) except (BadConnectionError, BleakError) as err: - _LOGGER.warning( - "Needed to disconnect due to error: %s (This can also be the case if you used device controls)", - err, - ) + # Ignore other errors + pass # caught an exception, return empty bytes object return bytes() diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index c85b4c8..de8bd40 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -42,6 +42,8 @@ class PollingCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=polling_interval), ) + self.address = address + # Create client self.logger.debug("Creating client") device = bluetooth.async_ble_device_from_address(hass, address) @@ -67,4 +69,10 @@ class PollingCoordinator(DataUpdateCoordinator): so entities can quickly look up their data. """ + # Check if device is connected + if bluetooth.async_address_present(self.hass, self.address, connectable=True) is False: + self.logger.warning("Device not connected") + self.last_update_success = False + return None + return await self.reader.read_data() diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 9390b8e..4b621b0 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -125,6 +125,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): self._attr_entity_category = category self._options = options + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._attr_available + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -133,10 +138,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): return if self.coordinator.data is None: - _LOGGER.warning( - "Data from coordinator is None. Skipping update for %s", - unique_id_loggable(self._attr_unique_id) + _LOGGER.debug( + "Data from coordinator is None", ) + self._attr_available = False + self.async_write_ha_state() return _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) @@ -145,12 +151,14 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): "Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False + self.async_write_ha_state() return response_data = self.coordinator.data.get(self._response_key) if response_data is None: _LOGGER.warning("No data for available for (%s)", self._response_key) self._attr_available = False + self.async_write_ha_state() return if ( @@ -167,6 +175,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): type(response_data), ) self._attr_available = False + self.async_write_ha_state() return self._attr_available = True diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 4481fdf..11efc32 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -108,6 +108,11 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): self._attr_entity_category = category self._attr_device_class = SwitchDeviceClass.OUTLET + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._attr_available + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -117,15 +122,17 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) if not isinstance(self.coordinator.data, dict): - _LOGGER.error( + _LOGGER.debug( "Invalid data from coordinator (switch.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False + self.async_write_ha_state() return response_data = self.coordinator.data.get(self._response_key) if response_data is None: self._attr_available = False + self.async_write_ha_state() return if not isinstance(response_data, bool): @@ -135,6 +142,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): response_data, ) self._attr_available = False + self.async_write_ha_state() return self._attr_available = True From 97154bb027a6c5a0ed629672d7d76874eb6ca00a Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 19:57:02 +0200 Subject: [PATCH 70/82] increase version number --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 1934a2c..be3733e 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.23" + "version": "0.0.24" } From 591c86baf591c502ad7e1514edea918f2d7f7a8c Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 20:22:43 +0200 Subject: [PATCH 71/82] fix internal_dc_input_voltage for ac300 and ac500 --- .../bluetti_bt/bluetti_bt_lib/devices/ac300.py | 2 +- .../bluetti_bt/bluetti_bt_lib/devices/ac500.py | 2 +- .../bluetti_bt/bluetti_bt_lib/utils/struct.py | 11 +++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 7a9cc8f..3f6b9e0 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -23,7 +23,7 @@ class AC300(ProtocolV1Device): self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) self.struct.add_uint_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) - self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 2def5c0..7d53243 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -23,7 +23,7 @@ class AC500(ProtocolV1Device): self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) self.struct.add_uint_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) - self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index 0e74706..10fd169 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -30,12 +30,15 @@ class DeviceField: class UintField(DeviceField): - def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]]): + def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], scale: int): self.range = range + self.scale = scale super().__init__(name, address, 1) def parse(self, data: bytes) -> int: - return struct.unpack("!H", data)[0] + val = struct.unpack("!H", data)[0] + + return val * self.scale def in_range(self, val: int) -> bool: if self.range is None: @@ -144,8 +147,8 @@ class DeviceStruct: def __init__(self): self.fields = [] - def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None): - self.fields.append(UintField(name, address, range)) + def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, scale: int = 1): + self.fields.append(UintField(name, address, range, scale)) def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None): self.fields.append(IntField(name, address, range)) From 221da9a38d70d0730d84c157bb632b4221aa2dd3 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 11:24:20 +0200 Subject: [PATCH 72/82] fix internal_power sensors for ac300 and ac500 --- .../bluetti_bt/bluetti_bt_lib/devices/ac300.py | 6 +++--- .../bluetti_bt/bluetti_bt_lib/devices/ac500.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 3f6b9e0..11845c1 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -15,13 +15,13 @@ class AC300(ProtocolV1Device): 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_current_one", 72, 1) - self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_int_field("internal_power_one", 73) self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_decimal_field("internal_current_two", 75, 1) - self.struct.add_uint_field("internal_power_two", 76) + self.struct.add_int_field("internal_power_two", 76) self.struct.add_decimal_field("ac_input_voltage", 77, 1) self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) - self.struct.add_uint_field("internal_power_three", 79) + self.struct.add_int_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 7d53243..b4d2b60 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -15,13 +15,13 @@ class AC500(ProtocolV1Device): 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_current_one", 72, 1) - self.struct.add_uint_field("internal_power_one", 73) + self.struct.add_int_field("internal_power_one", 73) self.struct.add_decimal_field("internal_ac_frequency", 74, 2) self.struct.add_decimal_field("internal_current_two", 75, 1) - self.struct.add_uint_field("internal_power_two", 76) + self.struct.add_int_field("internal_power_two", 76) self.struct.add_decimal_field("ac_input_voltage", 77, 1) self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) - self.struct.add_uint_field("internal_power_three", 79) + self.struct.add_int_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1) From 9af3d67ec6655c3a4cb0d0caf4f804a1a75a0019 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 11:34:25 +0200 Subject: [PATCH 73/82] removed binary sensors for disabled control entities to prevent duplicates --- custom_components/bluetti_bt/binary_sensor.py | 10 +--------- .../bluetti_bt/bluetti_bt_lib/utils/struct.py | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index ca6457a..0e09ee7 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -36,7 +36,7 @@ async def async_setup_entry( device_name = entry.data.get(CONF_NAME) address = entry.data.get(CONF_ADDRESS) - use_controls = entry.data.get(CONF_USE_CONTROLS) + use_controls = entry.data.get(CONF_USE_CONTROLS, False) if address is None: _LOGGER.error("Device has no address") @@ -52,12 +52,7 @@ async def async_setup_entry( for field_key, field_config in all_fields.items(): if bluetti_device.has_field(field_key): if field_config.type == FieldType.BOOL: - category = None if field_config.setter is True: - category = EntityCategory.DIAGNOSTIC - - # Ignore setters if controls enabled - if field_config.setter is True and use_controls is True: continue sensors_to_add.append( @@ -67,7 +62,6 @@ async def async_setup_entry( address, field_key, field_config.name, - category=category, ) ) @@ -84,7 +78,6 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): address, response_key: str, name: str, - category: EntityCategory | None = None, ): """Init battery entity.""" super().__init__(coordinator) @@ -98,7 +91,6 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): self._attr_name = name self._attr_available = False self._attr_unique_id = get_unique_id(e_name) - self._attr_entity_category = category @property def available(self) -> bool: diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index 10fd169..58a9d0c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -30,7 +30,7 @@ class DeviceField: class UintField(DeviceField): - def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], scale: int): + def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], scale: float): self.range = range self.scale = scale super().__init__(name, address, 1) @@ -147,7 +147,7 @@ class DeviceStruct: def __init__(self): self.fields = [] - def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, scale: int = 1): + def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, scale: float = 1): self.fields.append(UintField(name, address, range, scale)) def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None): From 2922c5cb906324e626d4683d7bb3cc9d96ea8de7 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 12:05:09 +0200 Subject: [PATCH 74/82] add multiplier to decimal field --- .../bluetti_bt/bluetti_bt_lib/utils/struct.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index 58a9d0c..70ef49e 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -30,15 +30,15 @@ class DeviceField: class UintField(DeviceField): - def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], scale: float): + def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], multiplier: float): self.range = range - self.scale = scale + self.multiplier = multiplier super().__init__(name, address, 1) def parse(self, data: bytes) -> int: val = struct.unpack("!H", data)[0] - return val * self.scale + return val * self.multiplier def in_range(self, val: int) -> bool: if self.range is None: @@ -82,15 +82,16 @@ class EnumField(DeviceField): class DecimalField(DeviceField): def __init__( - self, name: str, address: int, scale: int, range: Optional[Tuple[int, int]] + self, name: str, address: int, scale: int, range: Optional[Tuple[int, int]], multiplier: float ): self.scale = scale self.range = range + self.multiplier = multiplier super().__init__(name, address, 1) def parse(self, data: bytes) -> Decimal: val = Decimal(struct.unpack("!H", data)[0]) - return val / 10 ** self.scale + return (val / 10 ** self.scale) * self.multiplier def in_range(self, val: Decimal) -> bool: if self.range is None: @@ -147,8 +148,8 @@ class DeviceStruct: def __init__(self): self.fields = [] - def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, scale: float = 1): - self.fields.append(UintField(name, address, range, scale)) + def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, multiplier: float = 1): + self.fields.append(UintField(name, address, range, multiplier)) def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None): self.fields.append(IntField(name, address, range)) @@ -160,9 +161,9 @@ class DeviceStruct: self.fields.append(EnumField(name, address, enum)) def add_decimal_field( - self, name: str, address: int, scale: int, range: Tuple[int, int] = None + self, name: str, address: int, scale: int, range: Tuple[int, int] = None, multiplier: float = 1 ): - self.fields.append(DecimalField(name, address, scale, range)) + self.fields.append(DecimalField(name, address, scale, range, multiplier)) def add_decimal_array_field(self, name: str, address: int, size: int, scale: int): self.fields.append(DecimalArrayField(name, address, size, scale)) From b34d0a3039e8ea7fee68810938c6a1464939151e Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 12:06:21 +0200 Subject: [PATCH 75/82] fix ac300 and ac500 solar string values and renamed entity --- .../bluetti_bt/bluetti_bt_lib/devices/ac300.py | 6 +++--- .../bluetti_bt/bluetti_bt_lib/devices/ac500.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 11845c1..0b7df14 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -23,9 +23,9 @@ class AC300(ProtocolV1Device): self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) self.struct.add_int_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) - self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) - self.struct.add_decimal_field("internal_dc_input_power", 87, 1) - self.struct.add_decimal_field("internal_dc_input_current", 88, 2) + self.struct.add_uint_field("pv_input_voltage1", 86, multiplier=.1) + self.struct.add_decimal_field("pv_input_power1", 87, 1, multiplier=10) + self.struct.add_decimal_field("pv_input_current1", 88, 2, multiplier=10) # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index b4d2b60..8fc25d2 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -23,9 +23,9 @@ class AC500(ProtocolV1Device): self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) self.struct.add_int_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) - self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) - self.struct.add_decimal_field("internal_dc_input_power", 87, 1) - self.struct.add_decimal_field("internal_dc_input_current", 88, 2) + self.struct.add_uint_field("pv_input_voltage1", 86, multiplier=.1) + self.struct.add_decimal_field("pv_input_power1", 87, 1, multiplier=10) + self.struct.add_decimal_field("pv_input_current1", 88, 2, multiplier=10) # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal From 6e433e3551f3e00edd90cb71ebe74edf0c253fb6 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 12:14:51 +0200 Subject: [PATCH 76/82] fix total_battery_voltage --- custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py | 1 + custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py | 1 + custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py | 1 + custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py | 1 + custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py | 1 + custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py | 1 + 6 files changed, 6 insertions(+) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py index ab80a10..67be43d 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py @@ -42,6 +42,7 @@ class AC200L(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 5), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3060, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index 79feaea..aa574b1 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -42,6 +42,7 @@ class AC200M(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 5), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3060, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 0b7df14..b8fcb64 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -57,6 +57,7 @@ class AC300(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 8fc25d2..f4ac5ff 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -56,6 +56,7 @@ class AC500(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index 59101d6..3bf7192 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -52,6 +52,7 @@ class EP500(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index 62b62de..e3748ae 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -52,6 +52,7 @@ class EP500P(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), From b548528085f3b44e63ae4e5057531c64daafee0e Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 12:55:46 +0200 Subject: [PATCH 77/82] updated pack polling --- .../base_devices/BluettiDevice.py | 5 ++ .../base_devices/ProtocolV1Device.py | 6 ++ .../bluetti_bt_lib/bluetooth/device_reader.py | 55 +++++++++++-------- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py index dcc5cd7..44a16c3 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py @@ -42,6 +42,11 @@ class BluettiDevice: def writable_ranges(self) -> List[range]: """The address ranges that are writable""" return [] + + @property + def pack_num_field(self) -> List[ReadHoldingRegisters]: + """The address 'range' of the pack num""" + return [] def has_field(self, field: str): return any(f.name == field for f in self.struct.fields) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index 7f1f585..91bd5a4 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -56,3 +56,9 @@ class ProtocolV1Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: return [range(3006, 3009)] + + @property + def pack_num_field(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(3006, 1), + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py index 0e04564..a88beb0 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -97,20 +97,45 @@ class DeviceReader: _LOGGER.warning("Got a parse exception") # Execute pack polling commands - if len(pack_commands) > 0: + if len(pack_commands) > 0 and len(self.bluetti_device.pack_num_field) == 1: _LOGGER.debug("Polling battery packs") for pack in range(1, self.bluetti_device.pack_num_max + 1): + _LOGGER.debug("Setting pack_num to %i", pack) + # Set current pack number - await self._async_send_command( - self.bluetti_device.build_setter_command( - "pack_num", pack - ) + command = self.bluetti_device.build_setter_command( + "pack_num", pack ) + await self._async_send_command(command) - # We need to wait after switching packs for the data to be available - await asyncio.sleep(5) + try: + # Read pack_num field + command = self.bluetti_device.pack_num_field[0] + body = command.parse_response( + await self._async_send_command(command) + ) + _LOGGER.debug("Raw data: %s", body) + parsed = self.bluetti_device.parse( + command.starting_address, body + ) + _LOGGER.debug("Parsed data: %s", parsed) - pack_ok = False + # Check pack number + pack_number = parsed.get("pack_num") + if ( + not isinstance(pack_number, int) + or pack_number != pack + ): + _LOGGER.debug( + "Parsed pack_num(%s) does not match expected '%s', Skipping pack (maybe you don't have this connected)", + pack_number, + pack, + ) + continue + + except ParseError: + _LOGGER.warning("Got a pack parse exception, Skipping pack") + continue for command in pack_commands: # Request & parse result for each pack @@ -123,20 +148,6 @@ class DeviceReader: ) _LOGGER.debug("Parsed data: %s", parsed) - if not pack_ok: # Only check this once - pack_number = parsed.get("pack_num") - if ( - not isinstance(pack_number, int) - or pack_number != pack - ): - _LOGGER.debug( - "Parsed pack_num(%s) does not match expected '%s'", - pack_number, - pack, - ) - continue - pack_ok = True - for key, value in parsed.items(): parsed_data.update({key + str(pack): value}) From c3bfad0ea6c1fe1be6f2b53d6c1bc5fb2c519d93 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 13:46:09 +0200 Subject: [PATCH 78/82] add pack sensors if available --- custom_components/bluetti_bt/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 4b621b0..f437837 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -57,7 +57,7 @@ async def async_setup_entry( all_fields.update({name+str(pack): field}) for field_key, field_config in all_fields.items(): - if bluetti_device.has_field(field_key): + if bluetti_device.has_field(field_key) or (len(bluetti_device.pack_polling_commands) > 0 and field_key.startswith("pack_")): category = None if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS: category = EntityCategory.DIAGNOSTIC From 6be2abaaf2836f3ff991ae39cdc3709be4d62204 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 14:23:07 +0200 Subject: [PATCH 79/82] fix pack polling --- .../base_devices/BluettiDevice.py | 2 +- .../base_devices/ProtocolV1Device.py | 2 +- .../bluetti_bt_lib/bluetooth/device_reader.py | 39 ++++--------------- .../bluetti_bt_lib/devices/ac200l.py | 2 +- .../bluetti_bt_lib/devices/ac200m.py | 2 +- .../bluetti_bt_lib/devices/ac300.py | 2 +- .../bluetti_bt_lib/devices/ac500.py | 2 +- .../bluetti_bt_lib/devices/ep500.py | 2 +- .../bluetti_bt_lib/devices/ep500p.py | 2 +- custom_components/bluetti_bt/sensor.py | 2 +- 10 files changed, 16 insertions(+), 41 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py index 44a16c3..0d1edae 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py @@ -45,7 +45,7 @@ class BluettiDevice: @property def pack_num_field(self) -> List[ReadHoldingRegisters]: - """The address 'range' of the pack num""" + """The address 'range' of the pack num result. Matches pack_num_result""" return [] def has_field(self, field: str): diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index 91bd5a4..8160342 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -60,5 +60,5 @@ class ProtocolV1Device(BluettiDevice): @property def pack_num_field(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(3006, 1), + ReadHoldingRegisters(96, 1), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py index a88beb0..e721ded 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -106,36 +106,11 @@ class DeviceReader: command = self.bluetti_device.build_setter_command( "pack_num", pack ) - await self._async_send_command(command) - - try: - # Read pack_num field - command = self.bluetti_device.pack_num_field[0] - body = command.parse_response( - await self._async_send_command(command) - ) - _LOGGER.debug("Raw data: %s", body) - parsed = self.bluetti_device.parse( - command.starting_address, body - ) - _LOGGER.debug("Parsed data: %s", parsed) - - # Check pack number - pack_number = parsed.get("pack_num") - if ( - not isinstance(pack_number, int) - or pack_number != pack - ): - _LOGGER.debug( - "Parsed pack_num(%s) does not match expected '%s', Skipping pack (maybe you don't have this connected)", - pack_number, - pack, - ) - continue - - except ParseError: - _LOGGER.warning("Got a pack parse exception, Skipping pack") - continue + body = command.parse_response( + await self._async_send_command(command) + ) + _LOGGER.debug("Raw data set: %s", body) + # b'\x00\x02' b'\x00\x03' here (seems like result) for command in pack_commands: # Request & parse result for each pack @@ -198,9 +173,9 @@ class DeviceReader: return cast(bytes, res) except TimeoutError: - _LOGGER.warning("Polling single command timed out") + _LOGGER.debug("Polling single command timed out") except ModbusError as err: - _LOGGER.warning( + _LOGGER.debug( "Got an invalid request error for %s: %s", command, err, diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py index 67be43d..f4c16b5 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py @@ -24,7 +24,7 @@ class AC200L(ProtocolV1Device): # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_decimal_field("total_battery_voltage", 92, 1) - self.struct.add_uint_field("pack_num", 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_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index aa574b1..fb0d97c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -24,7 +24,7 @@ class AC200M(ProtocolV1Device): # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_decimal_field("total_battery_voltage", 92, 1) - self.struct.add_uint_field("pack_num", 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_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index b8fcb64..e7a9303 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -30,7 +30,7 @@ class AC300(ProtocolV1Device): # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_decimal_field("total_battery_voltage", 92, 1) - self.struct.add_uint_field("pack_num", 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_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index f4ac5ff..3159dde 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -30,7 +30,7 @@ class AC500(ProtocolV1Device): # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_decimal_field("total_battery_voltage", 92, 1) - self.struct.add_uint_field("pack_num", 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_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index 3bf7192..1554ce9 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -30,7 +30,7 @@ class EP500(ProtocolV1Device): # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_decimal_field("total_battery_voltage", 92, 1) - self.struct.add_uint_field("pack_num", 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_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index e3748ae..ba19241 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -30,7 +30,7 @@ class EP500P(ProtocolV1Device): # Battery packs self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_decimal_field("total_battery_voltage", 92, 1) - self.struct.add_uint_field("pack_num", 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_uint_field("pack_battery_percent", 99) self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index f437837..ba61abe 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -156,7 +156,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): response_data = self.coordinator.data.get(self._response_key) if response_data is None: - _LOGGER.warning("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.async_write_ha_state() return From 206099ab64e96165bf7b651032fd244693b67f2a Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 14:23:43 +0200 Subject: [PATCH 80/82] increase version number --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index be3733e..0afaf91 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.24" + "version": "0.0.25" } From 12a59346e968617ca0e60a365b21365eb4a65413 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 16:59:46 +0200 Subject: [PATCH 81/82] fix device type info --- .../bluetti_bt_lib/utils/device_builder.py | 2 +- custom_components/bluetti_bt/config_flow.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index e0e56c7..ed912b2 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -53,6 +53,6 @@ def build_device(address: str, name: str): def get_type_by_bt_name(bt_name: str): match = DEVICE_NAME_RE.match(bt_name) - if match is None or len(match) < 2: + if match is None: return "Unknown" return match[1] diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 0d20d13..545ff4e 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.const import CONF_ADDRESS, CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector @@ -48,11 +48,20 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() + if isinstance(discovery_info.name, str): + name = re.sub("[^A-Z0-9]+", "", discovery_info.name) + discovery_info.manufacturer_data = { + CONF_TYPE: get_type_by_bt_name(name) + } + # Get device type if needed if isinstance(discovery_info.name, str) and discovery_info.name.startswith("PBOX"): bleak_device = BleakClient(discovery_info.device) device_type = await recognize_device(bleak_device, self.hass.loop.create_future) _LOGGER.info("Device identified as %s", device_type) + discovery_info.manufacturer_data = { + CONF_TYPE: device_type.strip(), + } discovery_info.name = discovery_info.name.replace("PBOX", device_type.strip()) self._discovery_info = discovery_info @@ -76,6 +85,7 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={ CONF_ADDRESS: discovery_info.address, CONF_NAME: name, + CONF_TYPE: discovery_info.manufacturer_data.get(CONF_TYPE, "Unknown"), }, ) From 03ea349cbf195da27361f90af2527b1b6b3c5409 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Fri, 31 May 2024 17:04:41 +0200 Subject: [PATCH 82/82] set new version to 0.1 --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 0afaf91..e7030ff 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.25" + "version": "0.1.0" }