From ed10127a90223ecdf2e374227293b448b45ec60f Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 26 Aug 2023 14:44:03 +0200 Subject: [PATCH 1/6] added option to set polling interval --- custom_components/bluetti_bt/__init__.py | 5 +++-- custom_components/bluetti_bt/config_flow.py | 14 +++++++++++++- custom_components/bluetti_bt/const.py | 2 ++ custom_components/bluetti_bt/coordinator.py | 4 ++-- custom_components/bluetti_bt/translations/de.json | 4 +++- custom_components/bluetti_bt/translations/en.json | 4 +++- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 6c5a740..5b51176 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.exceptions import ConfigEntryNotReady -from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING +from .const import CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING from .coordinator import PollingCoordinator PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -25,6 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.data.get(CONF_ADDRESS) device_name = entry.data.get(CONF_NAME) use_controls = entry.data.get(CONF_USE_CONTROLS) + polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20) if address is None: return False @@ -38,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id].setdefault(DATA_POLLING_RUNNING, False) # Create coordinator for polling - coordinator = PollingCoordinator(hass, address, device_name) + coordinator = PollingCoordinator(hass, address, device_name, polling_interval) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator) diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 6e818e5..98a74ec 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from . import get_type_by_bt_name -from .const import DOMAIN, CONF_USE_CONTROLS +from .const import CONF_PERSISTANT_CONN, CONF_POLLING_INTERVAL, DOMAIN, CONF_USE_CONTROLS _LOGGER = logging.getLogger(__name__) @@ -114,6 +114,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): **self.config_entry.data, **{ CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS], + CONF_PERSISTANT_CONN: user_input[CONF_PERSISTANT_CONN], + CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL], }, }, ) @@ -126,6 +128,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): title="", data={ CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS], + CONF_PERSISTANT_CONN: user_input[CONF_PERSISTANT_CONN], + CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL], }, ) @@ -137,6 +141,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_USE_CONTROLS, default=self.config_entry.data.get(CONF_USE_CONTROLS, False), ): selector.BooleanSelector(), + vol.Required( + CONF_PERSISTANT_CONN, + default=self.config_entry.data.get(CONF_PERSISTANT_CONN, False), + ): selector.BooleanSelector(), + vol.Required( + CONF_POLLING_INTERVAL, + default=self.config_entry.data.get(CONF_POLLING_INTERVAL, 20), + ): int, } ), ) diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 2fdacb7..dd7baae 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -7,6 +7,8 @@ MANUFACTURER = "Bluetti" CONF_OPTIONS = "options" CONF_USE_CONTROLS = "use_controls" +CONF_PERSISTANT_CONN = "persistant_conn" +CONF_POLLING_INTERVAL = "polling_interval" DATA_COORDINATOR = "coordinator" DATA_POLLING_RUNNING = "polling_running" diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 0a62d41..b8e6694 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -129,13 +129,13 @@ class DummyDevice(BluettiDevice): class PollingCoordinator(DataUpdateCoordinator): """Polling coordinator.""" - def __init__(self, hass: HomeAssistant, address, device_name: str): + def __init__(self, hass: HomeAssistant, address, device_name: str, polling_interval: int): """Initialize coordinator.""" super().__init__( hass, _LOGGER, name="Bluetti polling coordinator", - update_interval=timedelta(seconds=20), + update_interval=timedelta(seconds=polling_interval), ) self._address = address self.notify_future = None diff --git a/custom_components/bluetti_bt/translations/de.json b/custom_components/bluetti_bt/translations/de.json index 8b1d377..850227c 100644 --- a/custom_components/bluetti_bt/translations/de.json +++ b/custom_components/bluetti_bt/translations/de.json @@ -3,7 +3,9 @@ "step": { "init": { "data": { - "use_controls": "Steuerung aktivieren (auf eigenes Risiko)" + "use_controls": "Steuerung aktivieren (auf eigenes Risiko)", + "persistant_conn": "Dauerhafte Verbindung", + "polling_interval": "Datenabruf-Intervall" } } } diff --git a/custom_components/bluetti_bt/translations/en.json b/custom_components/bluetti_bt/translations/en.json index d3fceda..e622734 100644 --- a/custom_components/bluetti_bt/translations/en.json +++ b/custom_components/bluetti_bt/translations/en.json @@ -3,7 +3,9 @@ "step": { "init": { "data": { - "use_controls": "Use controls (use at own risk)" + "use_controls": "Use controls (use at own risk)", + "persistant_conn": "Persistant connection", + "polling_interval": "Polling interval" } } } From 15216e1268619329e0372c212aaa010a0637f08f Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 26 Aug 2023 15:25:14 +0200 Subject: [PATCH 2/6] added option for persistant connection --- custom_components/bluetti_bt/__init__.py | 10 ++++- custom_components/bluetti_bt/config_flow.py | 2 +- custom_components/bluetti_bt/coordinator.py | 41 +++++++++++++++---- .../bluetti_bt/translations/de.json | 2 +- .../bluetti_bt/translations/en.json | 2 +- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 5b51176..5e73625 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.exceptions import ConfigEntryNotReady -from .const import CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING +from .const import CONF_PERSISTANT_CONN, CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING from .coordinator import PollingCoordinator PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -26,6 +26,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_name = entry.data.get(CONF_NAME) use_controls = entry.data.get(CONF_USE_CONTROLS) polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20) + persistant_conn = entry.data.get(CONF_PERSISTANT_CONN, False) + + # Prevent persistant connection and controls at same time + if use_controls is True and persistant_conn is True: + _LOGGER.warning("Enabling controls and persistant connection at the same time is currently not supported. Disabled persistant connection") + persistant_conn = False if address is None: return False @@ -39,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id].setdefault(DATA_POLLING_RUNNING, False) # Create coordinator for polling - coordinator = PollingCoordinator(hass, address, device_name, polling_interval) + coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistant_conn) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator) diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 98a74ec..6dcb071 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from . import get_type_by_bt_name -from .const import CONF_PERSISTANT_CONN, CONF_POLLING_INTERVAL, DOMAIN, CONF_USE_CONTROLS +from .const import CONF_PERSISTANT_CONN, CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, CONF_USE_CONTROLS _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index b8e6694..35a5f50 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -129,7 +129,14 @@ class DummyDevice(BluettiDevice): class PollingCoordinator(DataUpdateCoordinator): """Polling coordinator.""" - def __init__(self, hass: HomeAssistant, address, device_name: str, polling_interval: int): + def __init__( + self, + hass: HomeAssistant, + address: str, + device_name: str, + polling_interval: int, + persistant_conn: bool, + ): """Initialize coordinator.""" super().__init__( hass, @@ -138,10 +145,12 @@ class PollingCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=polling_interval), ) self._address = address + self.client = None self.notify_future = None self.current_command = None self.notify_response = bytearray() bluetti_device = build_device(address, device_name) + self.persistant_conn = persistant_conn # Add or modify device fields self.bluetti_device = DummyDevice(bluetti_device) @@ -166,16 +175,30 @@ class PollingCoordinator(DataUpdateCoordinator): return None # Polling - client = BleakClient(device) + self.client = BleakClient(device) parsed_data: dict = {} + # Connect to device + try: + async with async_timeout.timeout(5): + await self.client.connect() + + await self.client.start_notify( + BluetoothClient.NOTIFY_UUID, self._notification_handler + ) + except TimeoutError: + self.logger.warning("Connection timed out for device %s", self._address) + return None + except BleakError as err: + self.logger.warning("Bleak error: %s", err) + return None + try: async with async_timeout.timeout(15): - await client.connect() - await client.start_notify( - BluetoothClient.NOTIFY_UUID, self._notification_handler - ) + # Reconnect if not connected + if not self.client.is_connected: + await self.client.connect() for command in self.bluetti_device.polling_commands: try: @@ -186,7 +209,7 @@ class PollingCoordinator(DataUpdateCoordinator): # Make request self.logger.debug("Requesting %s", command) - await client.write_gatt_char( + await self.client.write_gatt_char( BluetoothClient.WRITE_UUID, bytes(command) ) @@ -229,7 +252,9 @@ class PollingCoordinator(DataUpdateCoordinator): self.logger.warning("Bleak error: %s", err) return None finally: - await client.disconnect() + # Disconnect if connection not persistant + if not self.persistant_conn: + await self.client.disconnect() self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = False diff --git a/custom_components/bluetti_bt/translations/de.json b/custom_components/bluetti_bt/translations/de.json index 850227c..abe7a9e 100644 --- a/custom_components/bluetti_bt/translations/de.json +++ b/custom_components/bluetti_bt/translations/de.json @@ -5,7 +5,7 @@ "data": { "use_controls": "Steuerung aktivieren (auf eigenes Risiko)", "persistant_conn": "Dauerhafte Verbindung", - "polling_interval": "Datenabruf-Intervall" + "polling_interval": "Datenabruf-Intervall in Sekunden (Neustart erforderlich)" } } } diff --git a/custom_components/bluetti_bt/translations/en.json b/custom_components/bluetti_bt/translations/en.json index e622734..5aa6eb7 100644 --- a/custom_components/bluetti_bt/translations/en.json +++ b/custom_components/bluetti_bt/translations/en.json @@ -5,7 +5,7 @@ "data": { "use_controls": "Use controls (use at own risk)", "persistant_conn": "Persistant connection", - "polling_interval": "Polling interval" + "polling_interval": "Polling interval in seconds (restart required)" } } } From a5ced35d32b549f06ae09265c560cad2d11f9667 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 26 Aug 2023 15:54:33 +0200 Subject: [PATCH 3/6] added validation for polling interval --- custom_components/bluetti_bt/binary_sensor.py | 10 +++++----- custom_components/bluetti_bt/config_flow.py | 5 +++++ custom_components/bluetti_bt/sensor.py | 2 +- custom_components/bluetti_bt/switch.py | 6 +++--- custom_components/bluetti_bt/translations/de.json | 5 ++++- custom_components/bluetti_bt/translations/en.json | 5 ++++- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index 67cfe38..ecf00d9 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Setup sensor entities.""" + """Setup binary_sensor entities.""" device_name = entry.data.get(CONF_NAME) address = entry.data.get(CONF_ADDRESS) @@ -43,7 +43,7 @@ async def async_setup_entry( _LOGGER.error("Device has no address") # Generate device info - _LOGGER.info("Creating sensors for device with address %s", address) + _LOGGER.info("Creating binary_sensors for device with address %s", address) device_info = dev_info(entry) # Add sensors according to device_info @@ -105,8 +105,8 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): """Handle updated data from the coordinator.""" _LOGGER.debug("Updating state of %s", self._attr_unique_id) if not isinstance(self.coordinator.data, dict): - _LOGGER.error( - "Invalid data from coordinator (sensor.%s)", self._attr_unique_id + _LOGGER.debug( + "Invalid data from coordinator (binary_sensor.%s)", self._attr_unique_id ) self._attr_available = False return @@ -118,7 +118,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): if not isinstance(response_data, bool): _LOGGER.warning( - "Invalid response data type from coordinator (sensor.%s): %s", + "Invalid response data type from coordinator (binary_sensor.%s): %s", self._attr_unique_id, response_data, ) diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 6dcb071..025f2ef 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -108,6 +108,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage the options.""" if user_input is not None: + + # Validate update interval + if user_input[CONF_POLLING_INTERVAL] < 5: + return self.async_abort(reason="invalid_interval") + changed = self.hass.config_entries.async_update_entry( self.config_entry, data={ diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 66ca581..1347396 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -131,7 +131,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): """Handle updated data from the coordinator.""" _LOGGER.debug("Updating state of %s", self._attr_unique_id) if not isinstance(self.coordinator.data, dict): - _LOGGER.error( + _LOGGER.debug( "Invalid data from coordinator (sensor.%s)", self._attr_unique_id ) self._attr_available = False diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 0d6a83e..2945226 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -50,7 +50,7 @@ async def async_setup_entry( _LOGGER.error("Device has no address") # Generate device info - _LOGGER.info("Creating sensors for device with address %s", address) + _LOGGER.info("Creating switches for device with address %s", address) device_info = dev_info(entry) # Add sensors according to device_info @@ -116,7 +116,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): _LOGGER.debug("Updating state of %s", self._attr_unique_id) if not isinstance(self.coordinator.data, dict): _LOGGER.error( - "Invalid data from coordinator (sensor.%s)", self._attr_unique_id + "Invalid data from coordinator (switch.%s)", self._attr_unique_id ) self._attr_available = False return @@ -128,7 +128,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): if not isinstance(response_data, bool): _LOGGER.warning( - "Invalid response data type from coordinator (sensor.%s): %s", + "Invalid response data type from coordinator (switch.%s): %s", self._attr_unique_id, response_data, ) diff --git a/custom_components/bluetti_bt/translations/de.json b/custom_components/bluetti_bt/translations/de.json index abe7a9e..b30d9f9 100644 --- a/custom_components/bluetti_bt/translations/de.json +++ b/custom_components/bluetti_bt/translations/de.json @@ -4,10 +4,13 @@ "init": { "data": { "use_controls": "Steuerung aktivieren (auf eigenes Risiko)", - "persistant_conn": "Dauerhafte Verbindung", + "persistant_conn": "Dauerhafte Verbindung (Neustart erforderlich)", "polling_interval": "Datenabruf-Intervall in Sekunden (Neustart erforderlich)" } } + }, + "abort": { + "invalid_interval": "Ungültiger Datenabruf-Intervall. Verwende 5 Sekunden oder mehr" } } } \ No newline at end of file diff --git a/custom_components/bluetti_bt/translations/en.json b/custom_components/bluetti_bt/translations/en.json index 5aa6eb7..3d0e7e6 100644 --- a/custom_components/bluetti_bt/translations/en.json +++ b/custom_components/bluetti_bt/translations/en.json @@ -4,10 +4,13 @@ "init": { "data": { "use_controls": "Use controls (use at own risk)", - "persistant_conn": "Persistant connection", + "persistant_conn": "Persistant connection (restart required)", "polling_interval": "Polling interval in seconds (restart required)" } } + }, + "abort": { + "invalid_interval": "Invalid polling interval. Use 5 seconds or more" } } } \ No newline at end of file From cf43c55cb8ce4cacc9b5dd96cf5c1b600909eef6 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 26 Aug 2023 15:56:10 +0200 Subject: [PATCH 4/6] fixed error on first device setup --- custom_components/bluetti_bt/coordinator.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 35a5f50..a167253 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -146,6 +146,7 @@ class PollingCoordinator(DataUpdateCoordinator): ) self._address = address self.client = None + self.has_notifier = False self.notify_future = None self.current_command = None self.notify_response = bytearray() @@ -174,18 +175,19 @@ class PollingCoordinator(DataUpdateCoordinator): self.logger.error("Device type for %s not found", self._address) return None - # Polling + # Create client self.client = BleakClient(device) parsed_data: dict = {} # Connect to device try: - async with async_timeout.timeout(5): + async with async_timeout.timeout(20): await self.client.connect() await self.client.start_notify( BluetoothClient.NOTIFY_UUID, self._notification_handler ) + self.has_notifier = True except TimeoutError: self.logger.warning("Connection timed out for device %s", self._address) return None @@ -200,6 +202,13 @@ class PollingCoordinator(DataUpdateCoordinator): if not self.client.is_connected: await self.client.connect() + # Attach notifier if needed + if not self.has_notifier: + await self.client.start_notify( + BluetoothClient.NOTIFY_UUID, self._notification_handler + ) + self.has_notifier = True + for command in self.bluetti_device.polling_commands: try: # Prepare to make request @@ -230,7 +239,7 @@ class PollingCoordinator(DataUpdateCoordinator): parsed_data.update(parsed) except TimeoutError: - self.logger.warning( + self.logger.debug( "Polling timed out (address: %s)", self._address ) except ParseError: @@ -246,7 +255,7 @@ class PollingCoordinator(DataUpdateCoordinator): "Needed to disconnect due to error: %s (This can also be the case if you used device controls)", err ) except TimeoutError: - self.logger.warning("Polling timed out for device %s", self._address) + self.logger.debug("Polling timed out for device %s", self._address) return None except BleakError as err: self.logger.warning("Bleak error: %s", err) From e0c7ada147cce896ce50764b67ba41aaf2e2d458 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 26 Aug 2023 16:00:41 +0200 Subject: [PATCH 5/6] updated 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 2d8115d..1fe3fa9 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -18,5 +18,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": ["bluetti_mqtt==0.15.0"], - "version": "0.0.8" + "version": "0.0.10" } From 43be1e1dbe6cbde937d724e5caa26e01f1485224 Mon Sep 17 00:00:00 2001 From: Patrick762 Date: Sat, 26 Aug 2023 18:01:05 +0200 Subject: [PATCH 6/6] added utility to make macs loggable --- custom_components/bluetti_bt/__init__.py | 10 +++--- custom_components/bluetti_bt/binary_sensor.py | 7 ++-- custom_components/bluetti_bt/config_flow.py | 10 +++--- custom_components/bluetti_bt/const.py | 2 +- custom_components/bluetti_bt/coordinator.py | 32 +++++++++---------- custom_components/bluetti_bt/sensor.py | 7 ++-- custom_components/bluetti_bt/switch.py | 15 +++++---- .../bluetti_bt/translations/de.json | 2 +- .../bluetti_bt/translations/en.json | 2 +- custom_components/bluetti_bt/utils.py | 12 +++++++ 10 files changed, 57 insertions(+), 42 deletions(-) create mode 100644 custom_components/bluetti_bt/utils.py diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 5e73625..107be83 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.exceptions import ConfigEntryNotReady -from .const import CONF_PERSISTANT_CONN, CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING +from .const import CONF_PERSISTENT_CONN, CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING from .coordinator import PollingCoordinator PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -26,12 +26,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_name = entry.data.get(CONF_NAME) use_controls = entry.data.get(CONF_USE_CONTROLS) polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20) - persistant_conn = entry.data.get(CONF_PERSISTANT_CONN, False) + persistent_conn = entry.data.get(CONF_PERSISTENT_CONN, False) # Prevent persistant connection and controls at same time - if use_controls is True and persistant_conn is True: + 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") - persistant_conn = False + persistent_conn = False if address is None: return False @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id].setdefault(DATA_POLLING_RUNNING, False) # Create coordinator for polling - coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistant_conn) + coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistent_conn) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator) diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index ecf00d9..b177e1c 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -28,6 +28,7 @@ from bluetti_mqtt.mqtt_client import ( from . import device_info as dev_info, get_unique_id from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator, DummyDevice +from .utils import unique_id_loggable _LOGGER = logging.getLogger(__name__) @@ -103,10 +104,10 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - _LOGGER.debug("Updating state of %s", self._attr_unique_id) + _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) if not isinstance(self.coordinator.data, dict): _LOGGER.debug( - "Invalid data from coordinator (binary_sensor.%s)", self._attr_unique_id + "Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False return @@ -119,7 +120,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): if not isinstance(response_data, bool): _LOGGER.warning( "Invalid response data type from coordinator (binary_sensor.%s): %s", - self._attr_unique_id, + unique_id_loggable(self._attr_unique_id), response_data, ) self._attr_available = False diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 025f2ef..d487271 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from . import get_type_by_bt_name -from .const import CONF_PERSISTANT_CONN, CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, CONF_USE_CONTROLS +from .const import CONF_PERSISTENT_CONN, CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, CONF_USE_CONTROLS _LOGGER = logging.getLogger(__name__) @@ -119,7 +119,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): **self.config_entry.data, **{ CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS], - CONF_PERSISTANT_CONN: user_input[CONF_PERSISTANT_CONN], + CONF_PERSISTENT_CONN: user_input[CONF_PERSISTENT_CONN], CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL], }, }, @@ -133,7 +133,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): title="", data={ CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS], - CONF_PERSISTANT_CONN: user_input[CONF_PERSISTANT_CONN], + CONF_PERSISTENT_CONN: user_input[CONF_PERSISTENT_CONN], CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL], }, ) @@ -147,8 +147,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.data.get(CONF_USE_CONTROLS, False), ): selector.BooleanSelector(), vol.Required( - CONF_PERSISTANT_CONN, - default=self.config_entry.data.get(CONF_PERSISTANT_CONN, False), + CONF_PERSISTENT_CONN, + default=self.config_entry.data.get(CONF_PERSISTENT_CONN, False), ): selector.BooleanSelector(), vol.Required( CONF_POLLING_INTERVAL, diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index dd7baae..acbf2ab 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -7,7 +7,7 @@ MANUFACTURER = "Bluetti" CONF_OPTIONS = "options" CONF_USE_CONTROLS = "use_controls" -CONF_PERSISTANT_CONN = "persistant_conn" +CONF_PERSISTENT_CONN = "persistent_conn" CONF_POLLING_INTERVAL = "polling_interval" DATA_COORDINATOR = "coordinator" diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index a167253..f8ded43 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -27,6 +27,7 @@ from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice from bluetti_mqtt.core.commands import ReadHoldingRegisters from .const import DATA_POLLING_RUNNING, DOMAIN +from .utils import mac_loggable _LOGGER = logging.getLogger(__name__) @@ -135,7 +136,7 @@ class PollingCoordinator(DataUpdateCoordinator): address: str, device_name: str, polling_interval: int, - persistant_conn: bool, + persistent_conn: bool, ): """Initialize coordinator.""" super().__init__( @@ -145,17 +146,23 @@ class PollingCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=polling_interval), ) self._address = address - self.client = None self.has_notifier = False self.notify_future = None self.current_command = None self.notify_response = bytearray() bluetti_device = build_device(address, device_name) - self.persistant_conn = persistant_conn + self.persistent_conn = persistent_conn # Add or modify device fields self.bluetti_device = DummyDevice(bluetti_device) + # Create client + device = bluetooth.async_ble_device_from_address(hass, address) + if device is None: + self.logger.error("Device %s not available", mac_loggable(address)) + return None + self.client = BleakClient(device) + async def _async_update_data(self): """Fetch data from API endpoint. @@ -166,17 +173,10 @@ class PollingCoordinator(DataUpdateCoordinator): self.logger.debug("Polling data") - device = bluetooth.async_ble_device_from_address(self.hass, self._address) - if device is None: - self.logger.error("Device %s not available", self._address) - return None - if self.bluetti_device is None: - self.logger.error("Device type for %s not found", self._address) + self.logger.error("Device type for %s not found", mac_loggable(self._address)) return None - # Create client - self.client = BleakClient(device) parsed_data: dict = {} # Connect to device @@ -189,10 +189,10 @@ class PollingCoordinator(DataUpdateCoordinator): ) self.has_notifier = True except TimeoutError: - self.logger.warning("Connection timed out for device %s", self._address) + self.logger.debug("Connection timed out for device %s", mac_loggable(self._address)) return None except BleakError as err: - self.logger.warning("Bleak error: %s", err) + self.logger.debug("Bleak error: %s", err) return None try: @@ -240,7 +240,7 @@ class PollingCoordinator(DataUpdateCoordinator): except TimeoutError: self.logger.debug( - "Polling timed out (address: %s)", self._address + "Polling timed out (address: %s)", mac_loggable(self._address) ) except ParseError: self.logger.warning("Got a parse exception...") @@ -255,14 +255,14 @@ class PollingCoordinator(DataUpdateCoordinator): "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", self._address) + 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.persistant_conn: + if not self.persistent_conn: await self.client.disconnect() self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = False diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 1347396..9721f70 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -32,6 +32,7 @@ from bluetti_mqtt.mqtt_client import ( from . import device_info as dev_info, get_unique_id from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator, DummyDevice +from .utils import unique_id_loggable _LOGGER = logging.getLogger(__name__) @@ -129,10 +130,10 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - _LOGGER.debug("Updating state of %s", self._attr_unique_id) + _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) if not isinstance(self.coordinator.data, dict): _LOGGER.debug( - "Invalid data from coordinator (sensor.%s)", self._attr_unique_id + "Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False return @@ -151,7 +152,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): ): _LOGGER.warning( "Invalid response data type from coordinator (sensor.%s): %s has type %s", - self._attr_unique_id, + unique_id_loggable(self._attr_unique_id), response_data, type(response_data), ) diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index 2945226..6e4d5f0 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -35,6 +35,7 @@ from bluetti_mqtt.mqtt_client import ( from . import device_info as dev_info, get_unique_id from .const import CONTROL_FIELDS, DATA_COORDINATOR, DATA_POLLING_RUNNING, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator, DummyDevice +from .utils import mac_loggable, unique_id_loggable _LOGGER = logging.getLogger(__name__) @@ -113,10 +114,10 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - _LOGGER.debug("Updating state of %s", self._attr_unique_id) + _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) if not isinstance(self.coordinator.data, dict): _LOGGER.error( - "Invalid data from coordinator (switch.%s)", self._attr_unique_id + "Invalid data from coordinator (switch.%s)", unique_id_loggable(self._attr_unique_id) ) self._attr_available = False return @@ -129,7 +130,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): if not isinstance(response_data, bool): _LOGGER.warning( "Invalid response data type from coordinator (switch.%s): %s", - self._attr_unique_id, + unique_id_loggable(self._attr_unique_id), response_data, ) self._attr_available = False @@ -141,19 +142,19 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - _LOGGER.debug("Turn on %s on %s", self._response_key, self._address) + _LOGGER.debug("Turn on %s on %s", self._response_key, mac_loggable(self._address)) await self.write_to_device(True) async def async_turn_off(self, **kwargs): """Turn the entity off.""" - _LOGGER.debug("Turn off %s on %s", self._response_key, self._address) + _LOGGER.debug("Turn off %s on %s", self._response_key, mac_loggable(self._address)) await self.write_to_device(False) 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", self._address) + _LOGGER.error("Device %s not available", mac_loggable(self._address)) return command = self._bluetti_device.build_setter_command(self._response_key, state) @@ -177,7 +178,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): await asyncio.sleep(5) except TimeoutError: - _LOGGER.error("Timed out for device %s", self._address) + _LOGGER.error("Timed out for device %s", mac_loggable(self._address)) return None except BleakError as err: _LOGGER.error("Bleak error: %s", err) diff --git a/custom_components/bluetti_bt/translations/de.json b/custom_components/bluetti_bt/translations/de.json index b30d9f9..4e3ea74 100644 --- a/custom_components/bluetti_bt/translations/de.json +++ b/custom_components/bluetti_bt/translations/de.json @@ -4,7 +4,7 @@ "init": { "data": { "use_controls": "Steuerung aktivieren (auf eigenes Risiko)", - "persistant_conn": "Dauerhafte Verbindung (Neustart erforderlich)", + "persistent_conn": "Dauerhafte Verbindung (Neustart erforderlich)", "polling_interval": "Datenabruf-Intervall in Sekunden (Neustart erforderlich)" } } diff --git a/custom_components/bluetti_bt/translations/en.json b/custom_components/bluetti_bt/translations/en.json index 3d0e7e6..055b9a9 100644 --- a/custom_components/bluetti_bt/translations/en.json +++ b/custom_components/bluetti_bt/translations/en.json @@ -4,7 +4,7 @@ "init": { "data": { "use_controls": "Use controls (use at own risk)", - "persistant_conn": "Persistant connection (restart required)", + "persistent_conn": "Persistent connection (restart required)", "polling_interval": "Polling interval in seconds (restart required)" } } diff --git a/custom_components/bluetti_bt/utils.py b/custom_components/bluetti_bt/utils.py new file mode 100644 index 0000000..fce973c --- /dev/null +++ b/custom_components/bluetti_bt/utils.py @@ -0,0 +1,12 @@ +"""Tools.""" + +def mac_loggable(mac: str) -> str: + """Remove parts of the mac address for logging.""" + splitted = mac.split(":") + return "XX:XX:XX:XX:XX:" + splitted[-1] + +def unique_id_loggable(unique_id: str) -> str: + """Remove parts of the unique id for logging.""" + splitted = unique_id.split("_", maxsplit=1) + serial = splitted[0][:6] + return serial + "XXXXXXXXXX" + splitted[1]