From f5b8e09fb4093ed5be1a1f4338be4a8311c2aa35 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 15:03:47 +0200 Subject: [PATCH 1/4] prevent binary_sensor if controls enabled --- custom_components/bluetti_bt/binary_sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index 7737c63..ab235d4 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -22,7 +22,7 @@ from .bluetti_bt_lib.field_attributes import FIELD_ATTRIBUTES, FieldType from .bluetti_bt_lib.utils.device_builder import build_device from . import device_info as dev_info, get_unique_id -from .const import DATA_COORDINATOR, DOMAIN +from .const import DATA_COORDINATOR, DOMAIN, CONF_USE_CONTROLS from .coordinator import PollingCoordinator from .utils import unique_id_loggable @@ -36,6 +36,7 @@ async def async_setup_entry( device_name = entry.data.get(CONF_NAME) address = entry.data.get(CONF_ADDRESS) + use_controls = entry.data.get(CONF_USE_CONTROLS) if address is None: _LOGGER.error("Device has no address") @@ -55,6 +56,10 @@ async def async_setup_entry( if field_config.setter is True: category = EntityCategory.DIAGNOSTIC + # Ignore setters if controls enabled + if field_config.setter is True and use_controls is True: + continue + sensors_to_add.append( BluettiBinarySensor( hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], From b2672ec30290c15c4aeaeec1113783db7f91802a Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 19:56:23 +0200 Subject: [PATCH 2/4] fix availability states --- custom_components/bluetti_bt/binary_sensor.py | 8 ++++++++ .../bluetti_bt_lib/bluetooth/device_reader.py | 18 ++++++++++++------ custom_components/bluetti_bt/coordinator.py | 8 ++++++++ custom_components/bluetti_bt/sensor.py | 15 ++++++++++++--- custom_components/bluetti_bt/switch.py | 10 +++++++++- 5 files changed, 49 insertions(+), 10 deletions(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index ab235d4..ca6457a 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -100,6 +100,11 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): self._attr_unique_id = get_unique_id(e_name) self._attr_entity_category = category + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._attr_available + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -113,11 +118,13 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): "Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False + self.async_write_ha_state() return response_data = self.coordinator.data.get(self._response_key) if response_data is None: self._attr_available = False + self.async_write_ha_state() return if not isinstance(response_data, bool): @@ -127,6 +134,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): response_data, ) self._attr_available = False + self.async_write_ha_state() return self._attr_available = True diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py index c8cc38b..0e04564 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -144,7 +144,7 @@ class DeviceReader: _LOGGER.warning("Got a parse exception...") except TimeoutError: - _LOGGER.error("Polling timed out") + _LOGGER.warning("Polling timed out. Trying again later") return None except BleakError as err: _LOGGER.error("Bleak error: %s", err) @@ -153,10 +153,18 @@ class DeviceReader: # Disconnect if connection not persistant if not self.persistent_conn: if self.has_notifier: - await self.client.stop_notify(NOTIFY_UUID) + try: + await self.client.stop_notify(NOTIFY_UUID) + except: + # Ignore errors here + pass self.has_notifier = False await self.client.disconnect() + # Check if dict is empty + if not parsed_data: + return None + return parsed_data async def _async_send_command(self, command: ReadHoldingRegisters) -> bytes: @@ -187,10 +195,8 @@ class DeviceReader: err, ) except (BadConnectionError, BleakError) as err: - _LOGGER.warning( - "Needed to disconnect due to error: %s (This can also be the case if you used device controls)", - err, - ) + # Ignore other errors + pass # caught an exception, return empty bytes object return bytes() diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index c85b4c8..de8bd40 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -42,6 +42,8 @@ class PollingCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=polling_interval), ) + self.address = address + # Create client self.logger.debug("Creating client") device = bluetooth.async_ble_device_from_address(hass, address) @@ -67,4 +69,10 @@ class PollingCoordinator(DataUpdateCoordinator): so entities can quickly look up their data. """ + # Check if device is connected + if bluetooth.async_address_present(self.hass, self.address, connectable=True) is False: + self.logger.warning("Device not connected") + self.last_update_success = False + return None + return await self.reader.read_data() diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 9390b8e..4b621b0 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -125,6 +125,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): self._attr_entity_category = category self._options = options + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._attr_available + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -133,10 +138,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): return if self.coordinator.data is None: - _LOGGER.warning( - "Data from coordinator is None. Skipping update for %s", - unique_id_loggable(self._attr_unique_id) + _LOGGER.debug( + "Data from coordinator is None", ) + self._attr_available = False + self.async_write_ha_state() return _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) @@ -145,12 +151,14 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): "Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False + self.async_write_ha_state() return response_data = self.coordinator.data.get(self._response_key) if response_data is None: _LOGGER.warning("No data for available for (%s)", self._response_key) self._attr_available = False + self.async_write_ha_state() return if ( @@ -167,6 +175,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): type(response_data), ) self._attr_available = False + self.async_write_ha_state() return self._attr_available = True diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 4481fdf..11efc32 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -108,6 +108,11 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): self._attr_entity_category = category self._attr_device_class = SwitchDeviceClass.OUTLET + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._attr_available + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" @@ -117,15 +122,17 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) if not isinstance(self.coordinator.data, dict): - _LOGGER.error( + _LOGGER.debug( "Invalid data from coordinator (switch.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False + self.async_write_ha_state() return response_data = self.coordinator.data.get(self._response_key) if response_data is None: self._attr_available = False + self.async_write_ha_state() return if not isinstance(response_data, bool): @@ -135,6 +142,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): response_data, ) self._attr_available = False + self.async_write_ha_state() return self._attr_available = True From 97154bb027a6c5a0ed629672d7d76874eb6ca00a Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 19:57:02 +0200 Subject: [PATCH 3/4] increase version number --- custom_components/bluetti_bt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index 1934a2c..be3733e 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.23" + "version": "0.0.24" } From 591c86baf591c502ad7e1514edea918f2d7f7a8c Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Thu, 30 May 2024 20:22:43 +0200 Subject: [PATCH 4/4] fix internal_dc_input_voltage for ac300 and ac500 --- .../bluetti_bt/bluetti_bt_lib/devices/ac300.py | 2 +- .../bluetti_bt/bluetti_bt_lib/devices/ac500.py | 2 +- .../bluetti_bt/bluetti_bt_lib/utils/struct.py | 11 +++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 7a9cc8f..3f6b9e0 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -23,7 +23,7 @@ class AC300(ProtocolV1Device): self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) self.struct.add_uint_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) - self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 2def5c0..7d53243 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -23,7 +23,7 @@ class AC500(ProtocolV1Device): self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100)) self.struct.add_uint_field("internal_power_three", 79) self.struct.add_decimal_field("ac_input_frequency", 80, 2) - self.struct.add_uint_field("internal_dc_input_voltage", 86) + self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_current", 88, 2) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index 0e74706..10fd169 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -30,12 +30,15 @@ class DeviceField: class UintField(DeviceField): - def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]]): + def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], scale: int): self.range = range + self.scale = scale super().__init__(name, address, 1) def parse(self, data: bytes) -> int: - return struct.unpack("!H", data)[0] + val = struct.unpack("!H", data)[0] + + return val * self.scale def in_range(self, val: int) -> bool: if self.range is None: @@ -144,8 +147,8 @@ class DeviceStruct: def __init__(self): self.fields = [] - def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None): - self.fields.append(UintField(name, address, range)) + def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, scale: int = 1): + self.fields.append(UintField(name, address, range, scale)) def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None): self.fields.append(IntField(name, address, range))