Merge pull request #15 from SupaFlav/dev

Add battery pack percent & voltage
This commit is contained in:
Patrick762 authored and GitHub committed 2023-09-23 15:47:55 +02:00
commit 18e48092c0
4 files changed
+103 -46

No files matched your search

-5
View File
@@ -28,11 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20)
persistent_conn = entry.data.get(CONF_PERSISTENT_CONN, False)
# Prevent persistant connection and controls at same time
if use_controls is True and persistent_conn is True:
_LOGGER.warning("Enabling controls and persistant connection at the same time is currently not supported. Disabled persistant connection")
persistent_conn = False
if address is None:
return False
+83 -26
View File
@@ -80,6 +80,10 @@ class DummyDevice(BluettiDevice):
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":
@@ -112,7 +116,7 @@ class DummyDevice(BluettiDevice):
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return self._parent.pack_logging_commands
return self._parent.pack_polling_commands
@property
def logging_commands(self) -> List[ReadHoldingRegisters]:
@@ -163,6 +167,9 @@ class PollingCoordinator(DataUpdateCoordinator):
return None
self.client = BleakClient(device)
# polling mutex to guard against switches
self.polling_lock = asyncio.Lock()
async def _async_update_data(self):
"""Fetch data from API endpoint.
@@ -179,12 +186,23 @@ class PollingCoordinator(DataUpdateCoordinator):
parsed_data: dict = {}
async with self.polling_lock:
try:
async with async_timeout.timeout(15):
async with async_timeout.timeout(30):
# Reconnect if not connected
max_retries = 5
for attempt in range(1,max_retries + 1):
try:
if not self.client.is_connected:
await self.client.connect()
break
except Exception as e:
if attempt == max_retries:
raise # pass exception on max_retries attempt
else:
self.logger.debug(f"Connect unsucessful (attempt {attempt}): {e}. Retrying...")
await asyncio.sleep(2)
# Attach notifier if needed
if not self.has_notifier:
@@ -194,6 +212,66 @@ class PollingCoordinator(DataUpdateCoordinator):
self.has_notifier = True
for command in self.bluetti_device.polling_commands:
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)
parsed_data.update(parsed)
except ParseError:
self.logger.warning("Got a parse exception...")
# 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)
)
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)
packNo = parsed.get('pack_num')
if not isinstance(packNo, int) or packNo != pack:
self.logger.debug("Parsed pack_num(%s) does not match expected '%s'", packNo, 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.debug("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:
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
@@ -213,21 +291,12 @@ class PollingCoordinator(DataUpdateCoordinator):
# Process data
self.logger.debug("Got %s bytes", len(res))
response = cast(bytes, res)
body = command.parse_response(response)
parsed = self.bluetti_device.parse(
command.starting_address, body
)
self.logger.debug("Parsed data: %s", parsed)
parsed_data.update(parsed)
return cast(bytes, res)
except TimeoutError:
self.logger.debug(
"Polling timed out (address: %s)", mac_loggable(self._address)
)
except ParseError:
self.logger.warning("Got a parse exception...")
except ModbusError as err:
self.logger.warning(
"Got an invalid request error for %s: %s",
@@ -238,21 +307,9 @@ class PollingCoordinator(DataUpdateCoordinator):
self.logger.warning(
"Needed to disconnect due to error: %s (This can also be the case if you used device controls)", err
)
except TimeoutError:
self.logger.debug("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:
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
# caught an exception, return empty bytes object
return bytes()
def _notification_handler(self, _sender: int, data: bytearray):
"""Handle bt data."""
+9 -1
View File
@@ -27,6 +27,7 @@ from bluetti_mqtt.mqtt_client import (
NORMAL_DEVICE_FIELDS,
DC_INPUT_FIELDS,
MqttFieldType,
battery_pack_fields,
)
from . import device_info as dev_info, get_unique_id
@@ -59,8 +60,14 @@ async def async_setup_entry(
all_fields = NORMAL_DEVICE_FIELDS
all_fields.update(DC_INPUT_FIELDS)
all_fields.update(ADDITIONAL_DEVICE_FIELDS)
# 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():
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 field_config.id_override is not None:
category = None
if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS:
category = EntityCategory.DIAGNOSTIC
@@ -144,6 +151,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
_LOGGER.debug("No data for available for (%s)",self._response_key)
self._attr_available = False
return
+11 -14
View File
@@ -99,6 +99,9 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity):
super().__init__(coordinator)
self._bluetti_device = bluetti_device
self._coordinator = coordinator
self._client = coordinator.client
self._polling_lock = coordinator.polling_lock
e_name = f"{device_info.get('name')} {name}"
self._address = address
self._response_key = response_key
@@ -156,25 +159,17 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity):
async def write_to_device(self, state: bool):
"""Write to device."""
device = bluetooth.async_ble_device_from_address(self.hass, self._address)
if device is None:
_LOGGER.error("Device %s not available", mac_loggable(self._address))
return
command = self._bluetti_device.build_setter_command(self._response_key, state)
# Wait until polling is done
while self.hass.data[DOMAIN][self._entry_id][DATA_POLLING_RUNNING] is True:
_LOGGER.debug("Waiting for polling to finish...")
asyncio.sleep(1)
client = BleakClient(device)
async with self._polling_lock:
try:
async with async_timeout.timeout(15):
await client.connect()
if not self._client.is_connected:
await self._client.connect()
# Send command
await client.write_gatt_char(
_LOGGER.debug("Requesting %s (%s,%s)", command, self._response_key, state)
await self._client.write_gatt_char(
BluetoothClient.WRITE_UUID, bytes(command)
)
@@ -188,6 +183,8 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity):
_LOGGER.error("Bleak error: %s", err)
return None
finally:
await client.disconnect()
# Disconnect if connection not persistant
if not self._coordinator.persistent_conn:
await self._client.disconnect()
await self.coordinator.async_request_refresh()