diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index 7737c63..ca6457a 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], @@ -95,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.""" @@ -108,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): @@ -122,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/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)) 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/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" } 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