Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions bluetti_mqtt/bluetooth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, EP500, EP500P, EP600, EB3A, AC200L
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|EP500P|EP500|EP600|EB3A|AC200L)(\d+)$')


async def scan_devices():
Expand All @@ -27,6 +27,8 @@ def build_device(address: str, name: str):
match = DEVICE_NAME_RE.match(name)
if match[1] == 'AC200M':
return AC200M(address, match[2])
if match[1] == 'AC200L':
return AC200L(address, match[2])
if match[1] == 'AC300':
return AC300(address, match[2])
if match[1] == 'AC500':
Expand Down
1 change: 1 addition & 0 deletions bluetti_mqtt/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .devices.ep500p import EP500P
from .devices.ep600 import EP600
from .devices.eb3a import EB3A
from .devices.ac200l import AC200L
from .commands import (
DeviceCommand,
ReadHoldingRegisters,
Expand Down
111 changes: 111 additions & 0 deletions bluetti_mqtt/core/devices/ac200l.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from enum import Enum, unique
from typing import List
from ..commands import ReadHoldingRegisters
from .bluetti_device import BluettiDevice
from .struct import DeviceStruct


@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


@unique
class EcoShutdown(Enum):
ONE_HOUR = 1
TWO_HOURS = 2
THREE_HOURS = 3
FOUR_HOURS = 4


class AC200L(BluettiDevice):
def __init__(self, address: str, sn: str):
self.struct = DeviceStruct()

# # Core
self.struct.add_string_field('device_type', 10, 6)
self.struct.add_sn_field('serial_number', 17)
self.struct.add_version_field('arm_version', 23)
self.struct.add_version_field('dsp_version', 25)
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_decimal_field('power_generation', 41, 1) # Total power generated since last reset (kwh)
self.struct.add_uint_field('total_battery_percent', 43)
self.struct.add_bool_field('ac_output_on', 48)
self.struct.add_bool_field('dc_output_on', 49)

# # 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 Data
self.struct.add_uint_field('pack_num_max', 91)
self.struct.add_decimal_field('total_battery_voltage', 92, 2)
self.struct.add_uint_field('pack_num', 96)
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)

# # Controls
self.struct.add_uint_field('pack_num', 3006)
self.struct.add_bool_field('ac_output_on', 3007)
self.struct.add_bool_field('dc_output_on', 3008)
# 3031-3033 is the current device time & date without a timezone
self.struct.add_bool_field('power_off', 3060)
self.struct.add_enum_field('auto_sleep_mode', 3061, AutoSleepMode)
self.struct.add_bool_field('eco_on', 3063)
self.struct.add_enum_field('eco_shutdown', 3064, EcoShutdown)

super().__init__(address, 'AC200L', sn)

@property
def pack_num_max(self):
return 3

@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(10, 40),
ReadHoldingRegisters(70, 21),
ReadHoldingRegisters(3001, 64),
]

@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [ReadHoldingRegisters(91, 37)]

@property
def logging_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(0, 70),
ReadHoldingRegisters(70, 21),
ReadHoldingRegisters(3001, 61),
]

@property
def pack_logging_commands(self) -> List[ReadHoldingRegisters]:
return [ReadHoldingRegisters(91, 119)]

@property
def writable_ranges(self) -> List[range]:
return [range(3000, 3065)]
18 changes: 11 additions & 7 deletions bluetti_mqtt/mqtt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class MqttFieldConfig:
id_override: Optional[str] = None # Used to override Home Assistant field id


COMMAND_TOPIC_RE = re.compile(r'^bluetti/command/(\w+)-(\d+)/([a-z_]+)$')
COMMAND_TOPIC_RE = re.compile(r'bluetti/command/(\w+)-(\d+)/([a-z_]+)$')
NORMAL_DEVICE_FIELDS = {
'dc_input_power': MqttFieldConfig(
type=MqttFieldType.NUMERIC,
Expand Down Expand Up @@ -494,6 +494,7 @@ def __init__(
bus: EventBus,
hostname: str,
home_assistant_mode: str,
topic: str,
port: int = 1883,
username: Optional[str] = None,
password: Optional[str] = None,
Expand All @@ -504,6 +505,7 @@ def __init__(
self.username = username
self.password = password
self.home_assistant_mode = home_assistant_mode
self.topic = "custom-data" if topic == None else topic
self.devices = []

async def run(self):
Expand Down Expand Up @@ -535,8 +537,8 @@ async def handle_message(self, msg: ParserMessage):
await self.message_queue.put(msg)

async def _handle_commands(self, client: Client):
async with client.filtered_messages('bluetti/command/#') as messages:
await client.subscribe('bluetti/command/#')
async with client.filtered_messages(f'{self.topic}/bluetti/command/#') as messages:
await client.subscribe(f'{self.topic}/bluetti/command/#')
async for mqtt_message in messages:
await self._handle_command(mqtt_message)

Expand All @@ -558,8 +560,10 @@ async def _init_device(self, device: BluettiDevice, client: Client):

def payload(id: str, device: BluettiDevice, field: MqttFieldConfig) -> str:
ha_id = id if not field.id_override else field.id_override


payload_dict = {
'state_topic': f'bluetti/state/{device.type}-{device.sn}/{id}',
'state_topic': f'{self.topic}/bluetti/state/{device.type}-{device.sn}/{id}',
'device': {
'identifiers': [
f'{device.sn}'
Expand All @@ -572,7 +576,7 @@ def payload(id: str, device: BluettiDevice, field: MqttFieldConfig) -> str:
'object_id': f'{device.type}_{ha_id}',
}
if field.setter:
payload_dict['command_topic'] = f'bluetti/command/{device.type}-{device.sn}/{id}'
payload_dict['command_topic'] = f'{self.topic}/bluetti/command/{device.type}-{device.sn}/{id}'
payload_dict.update(field.home_assistant_extra)

return json.dumps(payload_dict, separators=(',', ':'))
Expand Down Expand Up @@ -632,7 +636,7 @@ def payload(id: str, device: BluettiDevice, field: MqttFieldConfig) -> str:

async def _handle_command(self, mqtt_message: MQTTMessage):
# Parse the mqtt_message.topic
m = COMMAND_TOPIC_RE.match(mqtt_message.topic)
m = COMMAND_TOPIC_RE.search(mqtt_message.topic)
if not m:
logging.warn(f'unknown command topic: {mqtt_message.topic}')
return
Expand Down Expand Up @@ -670,7 +674,7 @@ async def _handle_command(self, mqtt_message: MQTTMessage):

async def _handle_message(self, client: Client, msg: ParserMessage):
logging.debug(f'Got a message from {msg.device}: {msg.parsed}')
topic_prefix = f'bluetti/state/{msg.device.type}-{msg.device.sn}/'
topic_prefix = f'{self.topic}/bluetti/state/{msg.device.type}-{msg.device.sn}/'

# Publish normal fields
for name, value in msg.parsed.items():
Expand Down
5 changes: 5 additions & 0 deletions bluetti_mqtt/server_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def execute(self):
'--password',
type=str,
help='The optional MQTT broker password')
parser.add_argument(
'--topic',
type=str,
help='The MQTT broker topic - default: custom-data/bluetti/{state,command}/{device})')
parser.add_argument(
'--interval',
default=0,
Expand Down Expand Up @@ -107,6 +111,7 @@ async def run(self, args: argparse.Namespace):
bus=bus,
hostname=args.hostname,
home_assistant_mode=args.ha_config,
topic=args.topic,
port=args.port,
username=args.username,
password=args.password,
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
asyncio-mqtt==0.12.1
bleak==0.14.3
bleak==0.22.2
crcmod==1.7
dbus-next==0.2.3
paho-mqtt==1.6.1