diff --git a/bluetti_mqtt/bluetooth/__init__.py b/bluetti_mqtt/bluetooth/__init__.py index 5d5bea8..fe41887 100644 --- a/bluetti_mqtt/bluetooth/__init__.py +++ b/bluetti_mqtt/bluetooth/__init__.py @@ -3,13 +3,13 @@ from typing import Set from bleak import BleakScanner from bleak.backends.device import BLEDevice -from bluetti_mqtt.core import BluettiDevice, AC200M, AC300, AC500, AC60, EP500, EP500P, EP600, EB3A +from bluetti_mqtt.core import BluettiDevice, AC200M, AC300, AC500, AC60, AC70, EP500, EP500P, EP600, EB3A from .client import BluetoothClient from .exc import BadConnectionError, ModbusError, ParseError from .manager import MultiDeviceManager -DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|EP500P|EP500|EP600|EB3A)(\d+)$') +DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|AC70|EP500P|EP500|EP600|EB3A)(\d+)$') async def scan_devices(): @@ -33,6 +33,8 @@ def build_device(address: str, name: str): return AC500(address, match[2]) if match[1] == 'AC60': return AC60(address, match[2]) + if match[1] == 'AC70': + return AC70(address, match[2]) if match[1] == 'EP500': return EP500(address, match[2]) if match[1] == 'EP500P': diff --git a/bluetti_mqtt/core/__init__.py b/bluetti_mqtt/core/__init__.py index 2d16d41..0baedd1 100644 --- a/bluetti_mqtt/core/__init__.py +++ b/bluetti_mqtt/core/__init__.py @@ -3,6 +3,7 @@ from .devices.ac300 import AC300 from .devices.ac500 import AC500 from .devices.ac60 import AC60 +from .devices.ac70 import AC70 from .devices.ep500 import EP500 from .devices.ep500p import EP500P from .devices.ep600 import EP600 diff --git a/bluetti_mqtt/core/devices/ac60.py b/bluetti_mqtt/core/devices/ac60.py index 69fce1b..822efd4 100644 --- a/bluetti_mqtt/core/devices/ac60.py +++ b/bluetti_mqtt/core/devices/ac60.py @@ -1,36 +1,119 @@ +from enum import Enum, unique from typing import List from ..commands import ReadHoldingRegisters from .bluetti_device import BluettiDevice from .struct import DeviceStruct +@unique +class LedMode(Enum): + OFF = 0 + LOW = 1 + HIGH = 2 + SOS = 3 + + +class ChargingMode(Enum): + STANDARD = 0 + SILENT = 1 + TURBO = 2 + + class AC60(BluettiDevice): def __init__(self, address: str, sn: str): self.struct = DeviceStruct() + # Core (100) self.struct.add_uint_field('total_battery_percent', 102) + self.struct.add_uint_field('estimated_time_min', 104) 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_uint_field('dc_output_power', 140) + self.struct.add_uint_field('ac_output_power', 142) + self.struct.add_uint_field('dc_input_power', 144) + self.struct.add_uint_field('ac_input_power', 146) + + # Input Details (1100 - 1300) self.struct.add_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('num_packs_connected', 1209) + self.struct.add_bool_field('charging_from_internal_dc', 1210) + self.struct.add_uint_field('internal_dc_input_power', 1212) + self.struct.add_decimal_field('internal_dc_input_voltage', 1213, 1) + self.struct.add_decimal_field('internal_dc_input_current', 1214, 1) + self.struct.add_bool_field('charging_from_pack_dc', 1218) + self.struct.add_uint_field('pack_dc_input_power', 1220) + self.struct.add_decimal_field('pack_dc_input_voltage', 1221, 1) + self.struct.add_decimal_field('pack_dc_input_current', 1222, 1) + self.struct.add_decimal_field('ac_input_frequency', 1300, 1) + self.struct.add_uint_field('internal_ac_input_power', 1313) + self.struct.add_decimal_field('ac_input_voltage', 1314, 1) + self.struct.add_decimal_field('ac_input_current', 1315, 1) + + # Output Details (1400 - 1500) + self.struct.add_uint_field('total_dc_output_power', 1400) + self.struct.add_uint_field('dc_usb_output_power', 1404) + self.struct.add_uint_field('dc_12v_output_power', 1406) + self.struct.add_uint_field('dc_output_uptime_minutes', 1410) + self.struct.add_uint_field('ac_output_power', 1420) + self.struct.add_uint_field('ac_output_uptime_minutes', 1424) + self.struct.add_uint_field('ac_output_power', 1430) + self.struct.add_decimal_field('ac_output_frequency', 1500, 1) + self.struct.add_bool_field('ac_output_on', 1509) + self.struct.add_uint_field('battery_inputoutput_power', 1510) + self.struct.add_decimal_field('ac_output_voltage', 1511, 1) + self.struct.add_decimal_field('ac_output_amps', 1512, 1) + + # Controls (2000) + self.struct.add_enum_field('led_mode', 2007, LedMode) + self.struct.add_bool_field('ac_output_on', 2011) + self.struct.add_bool_field('dc_output_on', 2012) + self.struct.add_bool_field('dc_eco_on', 2014) + self.struct.add_uint_field('dc_eco_hours', 2015) + self.struct.add_uint_field('dc_eco_watts', 2016) + self.struct.add_bool_field('ac_eco_on', 2017) + self.struct.add_uint_field('ac_eco_hours', 2018) + self.struct.add_uint_field('ac_eco_watts', 2019) + self.struct.add_enum_field('charging_mode', 2020, ChargingMode) + self.struct.add_bool_field('power_lifting_on', 2021) + + # More Controls (2200) + self.struct.add_bool_field('grid_enhancement_mode_on', 2225) + + # Battery Data Register (6000) + self.struct.add_decimal_field('total_battery_voltage', 6003, 2) + + # Battery Data Register (6100) self.struct.add_swap_string_field('battery_type', 6101, 6) - self.struct.add_sn_field('battery_serial_number', 6107) + self.struct.add_sn_field('pack_serial_number', 6107) + self.struct.add_decimal_field('pack_voltage', 6111, 2) + self.struct.add_uint_field('pack_battery_percent', 6113) self.struct.add_version_field('bcu_version', 6175) + # Battery Data Register (6300) + super().__init__(address, 'AC60', sn) @property def polling_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(100, 62), + ReadHoldingRegisters(100, 50), + ReadHoldingRegisters(1100, 51), + ReadHoldingRegisters(1200, 90), + ReadHoldingRegisters(1300, 31), + ReadHoldingRegisters(1400, 48), + ReadHoldingRegisters(1500, 30), + ReadHoldingRegisters(2000, 67), + ReadHoldingRegisters(2200, 29), + ReadHoldingRegisters(6000, 31), + ReadHoldingRegisters(6100, 100), + ReadHoldingRegisters(6300, 52), ] @property def logging_commands(self) -> List[ReadHoldingRegisters]: return [ - ReadHoldingRegisters(100, 62), + ReadHoldingRegisters(100, 50), ReadHoldingRegisters(1100, 51), ReadHoldingRegisters(1200, 90), ReadHoldingRegisters(1300, 31), @@ -42,3 +125,7 @@ def logging_commands(self) -> List[ReadHoldingRegisters]: ReadHoldingRegisters(6100, 100), ReadHoldingRegisters(6300, 52), ] + + @property + def writable_ranges(self) -> List[range]: + return [range(2000, 2225)] diff --git a/bluetti_mqtt/core/devices/ac70.py b/bluetti_mqtt/core/devices/ac70.py new file mode 100644 index 0000000..0528a71 --- /dev/null +++ b/bluetti_mqtt/core/devices/ac70.py @@ -0,0 +1,122 @@ +from enum import Enum, unique +from typing import List +from ..commands import ReadHoldingRegisters +from .bluetti_device import BluettiDevice +from .struct import DeviceStruct + + +@unique + +class ChargingMode(Enum): + STANDARD = 0 + SILENT = 1 + TURBO = 2 + + +class AC70(BluettiDevice): + def __init__(self, address: str, sn: str): + self.struct = DeviceStruct() + + # Core (100) + self.struct.add_uint_field('total_battery_percent', 102) + self.struct.add_decimal_field('estimated_time_hr', 104,1) + self.struct.add_swap_string_field('device_type', 110, 6) + self.struct.add_sn_field('serial_number', 116) + self.struct.add_uint_field('dc_output_power', 140) + self.struct.add_uint_field('ac_output_power', 142) + self.struct.add_uint_field('dc_input_power', 144) + self.struct.add_uint_field('ac_input_power', 146) + + # Input Details (1100 - 1300) + self.struct.add_swap_string_field('device_type', 1101, 6) + self.struct.add_sn_field('serial_number', 1107) + self.struct.add_uint_field('num_packs_connected', 1209) + self.struct.add_bool_field('charging_from_internal_dc', 1210) + self.struct.add_uint_field('internal_dc_input_power', 1212) + self.struct.add_decimal_field('internal_dc_input_voltage', 1213, 1) + self.struct.add_decimal_field('internal_dc_input_current', 1214, 1) + self.struct.add_bool_field('charging_from_pack_dc', 1218) + self.struct.add_uint_field('pack_dc_input_power', 1220) + self.struct.add_decimal_field('pack_dc_input_voltage', 1221, 1) + self.struct.add_decimal_field('pack_dc_input_current', 1222, 1) + self.struct.add_decimal_field('ac_input_frequency', 1300, 1) + self.struct.add_uint_field('internal_ac_input_power', 1313) + self.struct.add_decimal_field('ac_input_voltage', 1314, 1) + self.struct.add_decimal_field('ac_input_current', 1315, 1) + + # Output Details (1400 - 1500) + self.struct.add_uint_field('total_dc_output_power', 1400) + self.struct.add_uint_field('dc_usb_output_power', 1404) + self.struct.add_uint_field('dc_12v_output_power', 1406) + self.struct.add_uint_field('dc_output_uptime_minutes', 1410) + self.struct.add_uint_field('ac_output_power', 1420) + self.struct.add_uint_field('ac_output_uptime_minutes', 1424) + self.struct.add_uint_field('ac_output_power', 1430) + self.struct.add_decimal_field('ac_output_frequency', 1500, 1) + self.struct.add_bool_field('ac_output_on', 1509) + self.struct.add_uint_field('battery_inputoutput_power', 1510) + self.struct.add_decimal_field('ac_output_voltage', 1511, 1) + self.struct.add_decimal_field('ac_output_amps', 1512, 1) + + # Controls (2000) + self.struct.add_bool_field('ac_output_on', 2011) + self.struct.add_bool_field('dc_output_on', 2012) + self.struct.add_bool_field('dc_eco_on', 2014) + self.struct.add_uint_field('dc_eco_hours', 2015) + self.struct.add_uint_field('dc_eco_watts', 2016) + self.struct.add_bool_field('ac_eco_on', 2017) + self.struct.add_uint_field('ac_eco_hours', 2018) + self.struct.add_uint_field('ac_eco_watts', 2019) + self.struct.add_enum_field('charging_mode', 2020, ChargingMode) + self.struct.add_bool_field('power_lifting_on', 2021) + + # More Controls (2200) + self.struct.add_bool_field('grid_enhancement_mode_on', 2225) + + # Battery Data Register (6000) + self.struct.add_decimal_field('total_battery_voltage', 6003, 2) + + # Battery Data Register (6100) + self.struct.add_swap_string_field('battery_type', 6101, 6) + self.struct.add_sn_field('pack_serial_number', 6107) + self.struct.add_decimal_field('pack_voltage', 6111, 2) + self.struct.add_uint_field('pack_battery_percent', 6113) + self.struct.add_version_field('bcu_version', 6175) + + super().__init__(address, 'AC70', sn) + + @property + def polling_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(100, 50), + ReadHoldingRegisters(1100, 51), + ReadHoldingRegisters(1200, 90), + ReadHoldingRegisters(1300, 31), + ReadHoldingRegisters(1400, 48), + ReadHoldingRegisters(1500, 30), + ReadHoldingRegisters(2000, 67), + ReadHoldingRegisters(2200, 29), + ReadHoldingRegisters(6000, 31), + ReadHoldingRegisters(6100, 100), + ReadHoldingRegisters(6300, 52), + ] + + @property + def logging_commands(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(100, 50), + ReadHoldingRegisters(1100, 51), + ReadHoldingRegisters(1200, 90), + ReadHoldingRegisters(1300, 31), + ReadHoldingRegisters(1400, 48), + ReadHoldingRegisters(1500, 30), + ReadHoldingRegisters(2000, 67), + ReadHoldingRegisters(2200, 29), + ReadHoldingRegisters(6000, 31), + ReadHoldingRegisters(6100, 100), + ReadHoldingRegisters(6300, 52), + ] + + @property + def writable_ranges(self) -> List[range]: + return [range(2000, 2225)] diff --git a/bluetti_mqtt/mqtt_client.py b/bluetti_mqtt/mqtt_client.py index dd62c4c..7ad7402 100644 --- a/bluetti_mqtt/mqtt_client.py +++ b/bluetti_mqtt/mqtt_client.py @@ -78,6 +78,30 @@ class MqttFieldConfig: 'force_update': True, } ), + 'dc_usb_output_power': MqttFieldConfig( + type=MqttFieldType.NUMERIC, + setter=False, + advanced=False, + home_assistant_extra={ + 'name': 'DC USB Output Power', + 'unit_of_measurement': 'W', + 'device_class': 'power', + 'state_class': 'measurement', + 'force_update': True, + } + ), + 'dc_12v_output_power': MqttFieldConfig( + type=MqttFieldType.NUMERIC, + setter=False, + advanced=False, + home_assistant_extra={ + 'name': 'DC 12V Output Power', + 'unit_of_measurement': 'W', + 'device_class': 'power', + 'state_class': 'measurement', + 'force_update': True, + } + ), 'power_generation': MqttFieldConfig( type=MqttFieldType.NUMERIC, setter=False, @@ -126,6 +150,30 @@ class MqttFieldConfig: 'name': 'AC Output Mode', } ), + 'ac_input_voltage': MqttFieldConfig( + type=MqttFieldType.NUMERIC, + setter=False, + advanced=False, + home_assistant_extra={ + 'name': 'AC Input Voltage', + 'unit_of_measurement': 'V', + 'device_class': 'voltage', + 'state_class': 'measurement', + 'force_update': True, + } + ), + 'ac_input_current': MqttFieldConfig( + type=MqttFieldType.NUMERIC, + setter=False, + advanced=False, + home_assistant_extra={ + 'name': 'AC Input Current', + 'unit_of_measurement': 'A', + 'device_class': 'current', + 'state_class': 'measurement', + 'force_update': True, + } + ), 'internal_ac_voltage': MqttFieldConfig( type=MqttFieldType.NUMERIC, setter=False, @@ -198,18 +246,6 @@ class MqttFieldConfig: 'force_update': True, } ), - 'ac_input_voltage': MqttFieldConfig( - type=MqttFieldType.NUMERIC, - setter=False, - advanced=True, - home_assistant_extra={ - 'name': 'AC Input Voltage', - 'unit_of_measurement': 'V', - 'device_class': 'voltage', - 'state_class': 'measurement', - 'force_update': True, - } - ), 'internal_current_three': MqttFieldConfig( type=MqttFieldType.NUMERIC, setter=False, @@ -237,7 +273,7 @@ class MqttFieldConfig: 'ac_input_frequency': MqttFieldConfig( type=MqttFieldType.NUMERIC, setter=False, - advanced=True, + advanced=False, home_assistant_extra={ 'name': 'AC Input Frequency', 'unit_of_measurement': 'Hz', @@ -373,6 +409,24 @@ class MqttFieldConfig: 'icon': 'mdi:sprout', } ), + 'dc_eco_on': MqttFieldConfig( + type=MqttFieldType.BOOL, + setter=True, + advanced=False, + home_assistant_extra={ + 'name': 'DC ECO', + 'icon': 'mdi:sprout', + } + ), + 'ac_eco_on': MqttFieldConfig( + type=MqttFieldType.BOOL, + setter=True, + advanced=False, + home_assistant_extra={ + 'name': 'AC ECO', + 'icon': 'mdi:sprout', + } + ), 'eco_shutdown': MqttFieldConfig( type=MqttFieldType.ENUM, setter=True, @@ -402,6 +456,35 @@ class MqttFieldConfig: 'icon': 'mdi:arm-flex', } ), + 'grid_enhancement_mode_on': MqttFieldConfig( + type=MqttFieldType.BOOL, + setter=True, + advanced=False, + home_assistant_extra={ + 'name': 'Grid Enhancement Mode', + 'icon': 'mdi:transmission-tower', + } + ), + 'estimated_time_min': MqttFieldConfig( + type=MqttFieldType.NUMERIC, + setter=False, + advanced=False, + home_assistant_extra={ + 'name': 'Estimated Charge/Run Time', + 'unit_of_measurement': 'min', + 'icon': 'mdi:update', + } + ), + 'estimated_time_hr': MqttFieldConfig( + type=MqttFieldType.NUMERIC, + setter=False, + advanced=False, + home_assistant_extra={ + 'name': 'Estimated Charge/Run Time', + 'unit_of_measurement': 'h', + 'icon': 'mdi:update', + } + ), } DC_INPUT_FIELDS = { 'dc_input_voltage1': MqttFieldConfig(