diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 300ef90..45b7765 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_PERSISTENT_CONN, CONF_POLLING_INTERVAL, DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING +from .const import CONF_MAX_RETRIES, CONF_PERSISTENT_CONN, CONF_POLLING_INTERVAL, CONF_POLLING_TIMEOUT, DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING from .coordinator import PollingCoordinator PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -27,6 +27,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: use_controls = entry.data.get(CONF_USE_CONTROLS) polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20) persistent_conn = entry.data.get(CONF_PERSISTENT_CONN, False) + polling_timeout = entry.data.get(CONF_POLLING_TIMEOUT, 45) + max_retries = entry.data.get(CONF_MAX_RETRIES, 5) if address is None: return False @@ -40,7 +42,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, persistent_conn) + coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistent_conn, polling_timeout, max_retries) 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 975a068..61d78c9 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -17,7 +17,14 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from . import get_type_by_bt_name -from .const import CONF_PERSISTENT_CONN, CONF_POLLING_INTERVAL, DOMAIN, CONF_USE_CONTROLS +from .const import ( + CONF_MAX_RETRIES, + CONF_PERSISTENT_CONN, + CONF_POLLING_INTERVAL, + CONF_POLLING_TIMEOUT, + CONF_USE_CONTROLS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -112,6 +119,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): # Validate update interval if user_input[CONF_POLLING_INTERVAL] < 5: return self.async_abort(reason="invalid_interval") + + # Validate update timeout + if user_input[CONF_POLLING_TIMEOUT] < 1: + return self.async_abort(reason="invalid_timeout") + + # Validate max retries + if user_input[CONF_MAX_RETRIES] < 1: + return self.async_abort(reason="invalid_retries") changed = self.hass.config_entries.async_update_entry( self.config_entry, @@ -121,6 +136,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS], CONF_PERSISTENT_CONN: user_input[CONF_PERSISTENT_CONN], CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL], + CONF_POLLING_TIMEOUT: user_input[CONF_POLLING_TIMEOUT], + CONF_MAX_RETRIES: user_input[CONF_MAX_RETRIES], }, }, ) @@ -135,6 +152,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS], CONF_PERSISTENT_CONN: user_input[CONF_PERSISTENT_CONN], CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL], + CONF_POLLING_TIMEOUT: user_input[CONF_POLLING_TIMEOUT], + CONF_MAX_RETRIES: user_input[CONF_MAX_RETRIES], }, ) @@ -154,6 +173,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_POLLING_INTERVAL, default=self.config_entry.data.get(CONF_POLLING_INTERVAL, 20), ): int, + vol.Required( + CONF_POLLING_TIMEOUT, + default=self.config_entry.data.get(CONF_POLLING_TIMEOUT, 45), + ): int, + vol.Required( + CONF_MAX_RETRIES, + default=self.config_entry.data.get(CONF_MAX_RETRIES, 5), + ): int, } ), ) diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 202cf06..75c2406 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -9,6 +9,8 @@ CONF_OPTIONS = "options" CONF_USE_CONTROLS = "use_controls" CONF_PERSISTENT_CONN = "persistent_conn" CONF_POLLING_INTERVAL = "polling_interval" +CONF_POLLING_TIMEOUT = "polling_timeout" +CONF_MAX_RETRIES = "max_retries" 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 9aed097..6f630a9 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -135,6 +135,8 @@ class PollingCoordinator(DataUpdateCoordinator): device_name: str, polling_interval: int, persistent_conn: bool, + polling_timeout: int, + max_retries: int, ): """Initialize coordinator.""" super().__init__( @@ -150,6 +152,8 @@ class PollingCoordinator(DataUpdateCoordinator): self.notify_response = bytearray() bluetti_device = build_device(address, device_name) self.persistent_conn = persistent_conn + self.polling_timeout = polling_timeout + self.max_retries = max_retries # Add or modify device fields self.bluetti_device = DummyDevice(bluetti_device) @@ -183,17 +187,16 @@ class PollingCoordinator(DataUpdateCoordinator): async with self.polling_lock: try: - async with async_timeout.timeout(45): + async with async_timeout.timeout(self.polling_timeout): # Reconnect if not connected - max_retries = 5 - for attempt in range(1,max_retries + 1): + for attempt in range(1, self.max_retries + 1): try: if not self.client.is_connected: await self.client.connect() break except Exception as e: - if attempt == max_retries: + if attempt == self.max_retries or attempt == 1: raise # pass exception on max_retries attempt else: self.logger.warning(f"Connect unsucessful (attempt {attempt}): {e}. Retrying...") @@ -211,6 +214,7 @@ class PollingCoordinator(DataUpdateCoordinator): body = command.parse_response( await self.async_send_command(command) ) + self.logger.debug("Raw data: %s", body) parsed = self.bluetti_device.parse( command.starting_address, body ) @@ -265,6 +269,7 @@ class PollingCoordinator(DataUpdateCoordinator): self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = False # Pass data back to sensors + # TODO No data if non persistent connection return parsed_data async def async_send_command(self, command: ReadHoldingRegisters) -> bytes: @@ -313,6 +318,7 @@ class PollingCoordinator(DataUpdateCoordinator): # Ignore notifications we don't expect if not self.notify_future or self.notify_future.done(): + _LOGGER.warning("Unexpected notification") return # If something went wrong, we might get weird data. diff --git a/custom_components/bluetti_bt/translations/de.json b/custom_components/bluetti_bt/translations/de.json index 4e3ea74..7093834 100644 --- a/custom_components/bluetti_bt/translations/de.json +++ b/custom_components/bluetti_bt/translations/de.json @@ -5,12 +5,16 @@ "data": { "use_controls": "Steuerung aktivieren (auf eigenes Risiko)", "persistent_conn": "Dauerhafte Verbindung (Neustart erforderlich)", - "polling_interval": "Datenabruf-Intervall in Sekunden (Neustart erforderlich)" + "polling_interval": "Datenabruf-Intervall in Sekunden (Neustart erforderlich)", + "polling_timeout": "Datenabruf-Timeout in Sekunden (Neustart erforderlich)", + "max_retries": "Maximale Verbindungsversuche (Neustart erforderlich)" } } }, "abort": { - "invalid_interval": "Ungültiger Datenabruf-Intervall. Verwende 5 Sekunden oder mehr" + "invalid_interval": "Ungültiger Datenabruf-Intervall. Verwende 5 Sekunden oder mehr", + "invalid_timeout": "Ungültiger Datenabruf-Timeout. Verwende 1 Sekunde oder mehr", + "invalid_retries": "Ungültige maximale Verbindungsversuche. Verwende 1 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 055b9a9..1229863 100644 --- a/custom_components/bluetti_bt/translations/en.json +++ b/custom_components/bluetti_bt/translations/en.json @@ -5,12 +5,16 @@ "data": { "use_controls": "Use controls (use at own risk)", "persistent_conn": "Persistent connection (restart required)", - "polling_interval": "Polling interval in seconds (restart required)" + "polling_interval": "Polling interval in seconds (restart required)", + "polling_timeout": "Polling timeout in seconds (restart required)", + "max_retries": "Maximum amount of connection retries (restart required)" } } }, "abort": { - "invalid_interval": "Invalid polling interval. Use 5 seconds or more" + "invalid_interval": "Invalid polling interval. Use 5 seconds or more", + "invalid_timeout": "Invalid polling timeout. Use 1 second or more", + "invalid_retries": "Invalid max retries. Use 1 or more" } } } \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 0c2854c..aa6ddcd 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,3 +1,5 @@ #!/bin/sh -cp -r ./custom_components/* /run/user/1000/gvfs/smb-share:*/custom_components/ +# This script is only for development purposes + +sudo cp -r ./custom_components/* /var/ha_config/custom_components/