Skip to content

Commit

Permalink
Made battery level update on change and fixed device reconnect
Browse files Browse the repository at this point in the history
  • Loading branch information
rabits committed Dec 25, 2024
1 parent 51a26b2 commit d0f4abc
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 32 deletions.
6 changes: 2 additions & 4 deletions custom_components/ef_ble/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async def async_step_bluetooth_confirm(
device = self._discovered_device
assert self._discovery_info is not None
discovery_info = self._discovery_info
title = discovery_info.name # TODO: Read user defined title of device here
title = device.name_by_user or device.name
_LOGGER.debug("Confirm discovery: %s, %s", title, user_input)
if user_input is not None:
return self.async_create_entry(title=title, data=user_input)
Expand Down Expand Up @@ -108,9 +108,7 @@ async def async_step_user(
discovery_info.device, discovery_info.advertisement
)
if device != None:
self._discovered_devices[address] = (
discovery_info.name # TODO: read user-defined title from device
)
self._discovered_devices[address] = device.name_by_user or device.name

if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
Expand Down
30 changes: 22 additions & 8 deletions custom_components/ef_ble/eflib/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def __init__(
self._connected = asyncio.Event()
self._disconnected = asyncio.Event()
self._retry_on_disconnect = False
self._retry_on_disconnect_delay = 10

self._enc_packet_buffer = b""

Expand Down Expand Up @@ -96,13 +97,12 @@ async def connect(self, max_attempts: int = MAX_CONNECT_ATTEMPTS):
)
except (asyncio.TimeoutError, BleakError) as err:
_LOGGER.error("%s: Failed to connect to the device: %s", self._address, err)
# Retry connection after error with a bit of delay
# loop = asyncio.get_event_loop()
# loop.call_later(5, asyncio.create_task, self.connect())
self.disconnected()
return

_LOGGER.info("%s: Connected", self._address)
self._retry_on_disconnect = True
self._retry_on_disconnect_delay = 10

if self._client._backend.__class__.__name__ == "BleakClientBlueZDBus":
await self._client._backend._acquire_mtu()
Expand All @@ -113,14 +113,28 @@ async def connect(self, max_attempts: int = MAX_CONNECT_ATTEMPTS):
await self.initBleSessionKey()

def disconnected(self, *args, **kwargs) -> None:
_LOGGER.info("%s: Disconnected from device", self._address)
_LOGGER.warning("%s: Disconnected from device", self._address)
if self._retry_on_disconnect:
loop = asyncio.get_event_loop()
loop.create_task(self.connect())
loop.create_task(self.reconnect())
else:
self._disconnected.set()

async def disconnect(self):
async def reconnect(self) -> None:
# Wait before reconnect
_LOGGER.warning(
"%s: Reconnecting to the device in %d seconds...",
self._address,
self._retry_on_disconnect_delay,
)
await asyncio.sleep(self._retry_on_disconnect_delay)
if not self._retry_on_disconnect:
_LOGGER.warning("%s: Reconnect is aborted", self._address)
return
self._retry_on_disconnect_delay += 10
await self.connect()

async def disconnect(self) -> None:
_LOGGER.info("%s: Disconnecting from device", self._address)
self._retry_on_disconnect = False
if self._client != None:
Expand Down Expand Up @@ -196,7 +210,7 @@ async def parseSimple(self, data: str):
_LOGGER.error(
"%s: parseSimple: Unable to parse simple packet - incorrect CRC16: %r",
self._address,
bytearray(data[: 6 + payload_length]).hex(),
bytearray(payload_data).hex(),
)
raise PacketParseError

Expand Down Expand Up @@ -248,7 +262,7 @@ async def parseEncPackets(self, data: str):
_LOGGER.error(
"%s: Unable to parse encrypted packet - incorrect CRC16: %r",
self._address,
bytearray(data[: 6 + payload_length]).hex(),
bytearray(payload_data).hex(),
)
continue

Expand Down
36 changes: 27 additions & 9 deletions custom_components/ef_ble/eflib/devicebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ def __init__(
self, ble_dev: BLEDevice, adv_data: AdvertisementData, sn: str
) -> None:
_LOGGER.debug(
"%s: Creating new device: %s '%s' (%s)",
"%s: Creating new device: %s (%s)",
ble_dev.address,
self.device,
adv_data.local_name,
sn,
)
self._ble_dev = ble_dev
self._address = ble_dev.address
self._name = adv_data.local_name
self._name_by_user = self._name
self._sn = sn
# We can't use advertisement name here - it's prone to change to "Ecoflow-dev"
self._name = self.NAME_PREFIX + self._sn[-4:]
self._name_by_user = self._name

self._conn = None
self._callbacks = set()
self._callbacks_map = dict()

@property
def device(self):
Expand Down Expand Up @@ -100,10 +101,27 @@ async def waitDisconnected(self):

await self._conn.waitDisconnected()

def register_callback(self, callback: Callable[[], None]) -> None:
def register_callback(
self, callback: Callable[[], None], propname: str | None
) -> None:
"""Register callback, called when Device changes state."""
self._callbacks.add(callback)

def remove_callback(self, callback: Callable[[], None]) -> None:
if propname is None:
self._callbacks.add(callback)
else:
self._callbacks_map[propname] = self._callbacks_map.get(
propname, set()
).union([callback])

def remove_callback(
self, callback: Callable[[], None], propname: str | None
) -> None:
"""Remove previously registered callback."""
self._callbacks.discard(callback)
if propname is None:
self._callbacks.discard(callback)
else:
self._callbacks_map.get(propname, set()).discard(callback)

def update_callback(self, propname: str) -> None:
"""Finding the registered callbacks in the map and then calling the callbacks"""
for callback in self._callbacks_map.get(propname, set()):
callback()
1 change: 1 addition & 0 deletions custom_components/ef_ble/eflib/devices/dpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Device(DeviceBase):
"""Delta Pro Ultra"""

SN_PREFIX = b"Y711"
NAME_PREFIX = "EF-YJ"

@staticmethod
def check(sn):
Expand Down
12 changes: 9 additions & 3 deletions custom_components/ef_ble/eflib/devices/shp2.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Device(DeviceBase):
"""Smart Home Panel 2"""

SN_PREFIX = b"HD31"
NAME_PREFIX = "EF-HD3"

NUM_OF_CIRCUITS = 12
NUM_OF_CHANNELS = 3
Expand Down Expand Up @@ -44,6 +45,13 @@ def battery_level(self) -> int | None:
"""Battery level as a percentage."""
return self._data_battery_level

@battery_level.setter
def battery_level(self, value: int) -> None:
"""Sets Battery level as a percentage and calls callbacks."""
if self._data_battery_level != value:
self._data_battery_level = value
self.update_callback("battery_level")

@property
def channel_power(self) -> list[int | None]:
"""Backup channels wattage in W."""
Expand Down Expand Up @@ -150,9 +158,7 @@ async def data_parse(self, packet: Packet) -> bool:
updated = True

if info.HasField("backup_bat_per"):
if self.battery_level != info.backup_bat_per:
self._data_battery_level = info.backup_bat_per
updated = True
self.battery_level = info.backup_bat_per

# TODO: Energy2_info.pv_height_charge_watts
# TODO: Energy2_info.pv_low_charge_watts
Expand Down
2 changes: 1 addition & 1 deletion custom_components/ef_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@
"protobuf"
],

"version": "0.3.0"
"version": "0.3.1"
}
31 changes: 24 additions & 7 deletions custom_components/ef_ble/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class SensorBase(SensorEntity):
_attr_has_entity_name = True
_attr_state_class = SensorStateClass.MEASUREMENT

# Name of the property to use to get a native value
ef_dev_property = "UNDEFINED"

def __init__(self, device):
"""Initialize the sensor."""
self._device = device
Expand All @@ -93,13 +96,29 @@ def available(self) -> bool:
"""Return True if device is connected."""
return self._device.is_connected

# Uses ef_dev_property to get the device sensor value
@property
def native_value(self) -> int | float | bool:
"""Return the value of the sensor."""
return getattr(self._device, self.ef_dev_property, None)

async def async_added_to_hass(self):
"""Run when this Entity has been added to HA."""
self._device.register_callback(self.async_write_ha_state)
if self.ef_dev_property != "UNDEFINED":
self._device.register_callback(
self.async_write_ha_state, self.ef_dev_property
)
else:
self._device.register_callback(self.async_write_ha_state, None)

async def async_will_remove_from_hass(self):
"""Entity being removed from hass."""
self._device.remove_callback(self.async_write_ha_state)
if self.ef_dev_property != "UNDEFINED":
self._device.remove_callback(
self.async_write_ha_state, self.ef_dev_property
)
else:
self._device.remove_callback(self.async_write_ha_state, None)


class BatterySensor(SensorBase):
Expand All @@ -108,18 +127,16 @@ class BatterySensor(SensorBase):
device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE

# Name of the property to use to get a native value
ef_dev_property = "battery_level"

def __init__(self, device):
"""Initialize the sensor."""
super().__init__(device)

self._attr_unique_id = f"{self._device.name}_battery"
self._attr_name = "Battery"

@property
def native_value(self) -> int:
"""Return the value of the sensor."""
return self._device.battery_level


class CircuitPowerSensor(SensorBase):
"""Represents circuit consumed wattage."""
Expand Down

0 comments on commit d0f4abc

Please sign in to comment.