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