From 91682b445d19546bc5dfe2d50c44fa415c6c770e Mon Sep 17 00:00:00 2001 From: fuatakgun Date: Thu, 9 Nov 2023 13:20:55 +0100 Subject: [PATCH] fix: better handling of reconnections --- README.md | 6 ++++-- custom_components/eufy_security/__init__.py | 6 ++++-- custom_components/eufy_security/camera.py | 4 ++++ .../eufy_security/config_flow.py | 19 ++++++++++++++--- .../eufy_security/coordinator.py | 4 ++++ .../eufy_security_api/api_client.py | 10 ++++----- .../eufy_security/eufy_security_api/camera.py | 21 +++++++++++++++++++ .../eufy_security_api/web_socket_client.py | 6 +++--- .../eufy_security/translations/de.json | 3 ++- .../eufy_security/translations/en.json | 3 ++- .../eufy_security/translations/fr.json | 3 ++- .../eufy_security/translations/it.json | 3 ++- .../eufy_security/translations/nl.json | 3 ++- .../eufy_security/translations/pl.json | 3 ++- .../eufy_security/translations/pt-BR.json | 3 ++- 15 files changed, 75 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d0b7a2c..49ba225 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ Welcome to Alpha release of Eufy Security Integration for Home Assistant. Congra - [3. Installing Eufy Security Integration](#3-installing-eufy-security-integration) - [4. Setting up your dashboard for camera](#4-setting-up-your-dashboard-for-camera) - [Features](#features) -- [Example Automation](#example-automation) - - [Start streaming on camera, when there is a motion, this would generate a new thumbnail on Home Assistant](#start-streaming-on-camera-when-there-is-a-motion-this-would-generate-a-new-thumbnail-on-home-assistant) +- [Example Automations](#example-automations) + - [Send notification with thumbnail from home assistant](#send-notification-with-thumbnail-from-home-assistant) + - [Alternative trigger condition](#alternative-trigger-condition) - [Unlock safe with code](#unlock-safe-with-code) - [Debugging Issues](#debugging-issues) - [Show Off](#show-off) @@ -342,6 +343,7 @@ target: # Debugging Issues First, check all issues (open or close) to find out if there was any similar question rather than duplicating it. +Focus on enabling push notification settings, lowering camera streaming/recording quality and removing any network level isoloation/restriction. Most of the issues could be eliminated via these. Later on, if you find a similar issue, please just put +1 on it, sharing same logs over and over does not help at all. Lastly, create your issue following the template. I will probably ask follow up questions later on. diff --git a/custom_components/eufy_security/__init__.py b/custom_components/eufy_security/__init__.py index 7a6b23e..9375b78 100644 --- a/custom_components/eufy_security/__init__.py +++ b/custom_components/eufy_security/__init__.py @@ -53,9 +53,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): hass.async_add_job(hass.config_entries.async_forward_entry_setup(config_entry, platform.value)) async def update(event_time_utc): - await coordinator.async_refresh() + local_coordinator = hass.data[DOMAIN][COORDINATOR] + await local_coordinator.async_refresh() + config_entry.add_update_listener(async_reload_entry) - async_track_time_interval(hass, update, timedelta(seconds=coordinator.config.sync_interval)) + # async_track_time_interval(hass, update, timedelta(seconds=coordinator.config.sync_interval)) return True diff --git a/custom_components/eufy_security/camera.py b/custom_components/eufy_security/camera.py index 56144ce..5671dbb 100644 --- a/custom_components/eufy_security/camera.py +++ b/custom_components/eufy_security/camera.py @@ -129,6 +129,10 @@ def is_streaming(self) -> bool: def available(self) -> bool: return True + @property + def extra_state_attributes(self): + return {"stream_debug": self.product.stream_debug} + async def _get_image_from_stream_url(self, width, height): while True: result = await ffmpeg.async_get_image(self.hass, await self.stream_source(), width=width, height=height) diff --git a/custom_components/eufy_security/config_flow.py b/custom_components/eufy_security/config_flow.py index 5937ccd..e13ebd2 100644 --- a/custom_components/eufy_security/config_flow.py +++ b/custom_components/eufy_security/config_flow.py @@ -8,6 +8,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later from .const import COORDINATOR, DOMAIN from .eufy_security_api.api_client import ApiClient @@ -60,7 +61,7 @@ def async_get_options_flow(config_entry: ConfigEntry): _LOGGER.debug(f"{DOMAIN} EufySecurityOptionFlowHandler - {config_entry.data}") return EufySecurityOptionFlowHandler(config_entry) - def __init__(self): + def __init__(self) -> None: self._errors = {} async def async_step_user(self, user_input=None): @@ -79,8 +80,20 @@ async def async_step_user(self, user_input=None): coordinator.config.captcha_img = None await coordinator.set_captcha_and_connect(captcha_id, captcha_input) - if self._async_current_entries(): - await self.hass.config_entries.async_reload(self.context["entry_id"]) + config_entry_id = None + for entry in self._async_current_entries(): + config_entry_id = entry.entry_id + + async def try_reloading(_now): + _LOGGER.debug(f"{DOMAIN} try_reloading start after captcha/mfa") + await coordinator.disconnect() + self.hass.data[DOMAIN] = {} + await self.hass.config_entries.async_reload(config_entry_id) + _LOGGER.debug(f"{DOMAIN} try_reloading finish after captcha/mfa") + + async_call_later(self.hass, 3, try_reloading) + return self.async_abort(reason="reauth_successful") + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/custom_components/eufy_security/coordinator.py b/custom_components/eufy_security/coordinator.py index 48e58ec..c574d09 100644 --- a/custom_components/eufy_security/coordinator.py +++ b/custom_components/eufy_security/coordinator.py @@ -83,7 +83,9 @@ async def set_log_level(self, log_level: str) -> None: async def _update_local(self): try: + _LOGGER.debug(f"coordinator - start update_local") await self._api.poll_refresh() + _LOGGER.debug(f"coordinator - complete update_local") return self.data except WebSocketConnectionException as exc: raise UpdateFailed(f"Error communicating with Add-on: {exc}") from exc @@ -91,6 +93,8 @@ async def _update_local(self): async def disconnect(self): """disconnect from api""" await self._api.disconnect() + self._api = None + await self.async_shutdown() async def _async_reload(self, _): await asyncio.sleep(5) diff --git a/custom_components/eufy_security/eufy_security_api/api_client.py b/custom_components/eufy_security/eufy_security_api/api_client.py index 6462ca1..a20ab12 100644 --- a/custom_components/eufy_security/eufy_security_api/api_client.py +++ b/custom_components/eufy_security/eufy_security_api/api_client.py @@ -130,14 +130,14 @@ async def _get_products(self, product_type: ProductType, products: list) -> dict async def set_captcha_and_connect(self, captcha_id: str, captcha_input: str): """Set captcha set products""" await self._set_captcha(captcha_id, captcha_input) - await asyncio.sleep(10) - await self._set_products() + await asyncio.sleep(30) + # await self._set_products() async def set_mfa_and_connect(self, mfa_input: str): """Set mfa code set products""" await self._set_mfa_code(mfa_input) - await asyncio.sleep(10) - await self._set_products() + await asyncio.sleep(30) + # await self._set_products() # server level commands async def _start_listening(self): @@ -256,7 +256,7 @@ async def reboot(self, product_type: ProductType, serial_no: str) -> None: await self._send_message_get_response(OutgoingMessage(OutgoingMessageType.reboot, serial_no=serial_no)) async def _on_message(self, message: dict) -> None: - message_str = str(message)[0:5000] + message_str = str(message)[0:1000] if "livestream video data" not in message_str and "livestream audio data" not in message_str: _LOGGER.debug(f"_on_message - {message_str}") if message[MessageField.TYPE.value] == IncomingMessageType.result.name: diff --git a/custom_components/eufy_security/eufy_security_api/camera.py b/custom_components/eufy_security/eufy_security_api/camera.py index e0edbc9..861dd47 100644 --- a/custom_components/eufy_security/eufy_security_api/camera.py +++ b/custom_components/eufy_security/eufy_security_api/camera.py @@ -75,6 +75,8 @@ def __init__(self, api, serial_no: str, properties: dict, metadata: dict, comman self.p2p_started_event = asyncio.Event() self.rtsp_started_event = asyncio.Event() + self.stream_debug = None + @property def is_streaming(self) -> bool: """Is Camera in Streaming Status""" @@ -133,25 +135,36 @@ async def start_livestream(self) -> bool: """Process start p2p livestream call""" self.set_stream_prodiver(StreamProvider.P2P) self.stream_status = StreamStatus.PREPARING + self.stream_debug = "info - send command to add-on" await self.api.start_livestream(self.product_type, self.serial_no) + self.stream_debug = "info - command was done, open a local tcp port" self.p2p_stream_thread = threading.Thread(target=self.p2p_stream_handler.setup, daemon=True) self.p2p_stream_thread.start() await wait_for_value(self.p2p_stream_handler.__dict__, "port", None) + self.stream_debug = "info - local tcp was setup, checking for codec" if self.codec is not None: + self.stream_debug = "info - codec is known, start ffmpeg consuming tcp port and forwarding to rtsp add-on" await self._start_ffmpeg() + self.stream_debug = "info - ffmpeg was started" with contextlib.suppress(asyncio.TimeoutError): + self.stream_debug = "info - wait for bytes to arrive from add-on, they will be written to tcp port" await asyncio.wait_for(self.p2p_started_event.wait(), STREAM_TIMEOUT_SECONDS) if self.p2p_started_event.is_set() is False: + self.stream_debug = "error - ffmpeg pocess could not connect" return False try: + self.stream_debug = "info - check if rtsp url is a valid stream" await asyncio.wait_for(self._is_stream_url_ready(), STREAM_TIMEOUT_SECONDS) except asyncio.TimeoutError: + self.stream_debug = "error - rtsp url was not a valid stream" return False + self.stream_status = StreamStatus.STREAMING + self.stream_debug = "info - streaming" return True async def stop_livestream(self): @@ -164,22 +177,30 @@ async def start_rtsp_livestream(self): """Process start rtsp livestream call""" self.set_stream_prodiver(StreamProvider.RTSP) self.stream_status = StreamStatus.PREPARING + self.stream_debug = "info - send command to add-on" await self.api.start_rtsp_livestream(self.product_type, self.serial_no) try: await asyncio.wait_for(self.rtsp_started_event.wait(), 5) + self.stream_debug = "info - command was done" except asyncio.TimeoutError: + self.stream_debug = "error - command was failed" return False try: self.stream_status = StreamStatus.STREAMING + self.stream_debug = "info - check if rtsp url is a valid stream" await asyncio.wait_for(self._is_stream_url_ready(), 5) _LOGGER.debug(f"start_rtsp_livestream - 2 - try success - {self.stream_status}") return True except asyncio.TimeoutError: + self.stream_debug = "error - rtsp url was not a valid stream" _LOGGER.debug("start_rtsp_livestream - 2 - try timeout") return False + self.stream_debug = "info - streaming" + return True + async def stop_rtsp_livestream(self): """Process stop rtsp livestream call""" await self.api.stop_rtsp_livestream(self.product_type, self.serial_no) diff --git a/custom_components/eufy_security/eufy_security_api/web_socket_client.py b/custom_components/eufy_security/eufy_security_api/web_socket_client.py index a62cf8c..2c15853 100644 --- a/custom_components/eufy_security/eufy_security_api/web_socket_client.py +++ b/custom_components/eufy_security/eufy_security_api/web_socket_client.py @@ -48,12 +48,12 @@ async def connect(self): async def disconnect(self): """Close web socket connection""" - if self.socket is not None: - await self.socket.close() - self.socket = None if self.task is not None: self.task.cancel() self.task = None + if self.socket is not None: + await self.socket.close() + self.socket = None async def _on_open(self) -> None: if self.open_callback is not None: diff --git a/custom_components/eufy_security/translations/de.json b/custom_components/eufy_security/translations/de.json index 2eb899e..37a4ba4 100644 --- a/custom_components/eufy_security/translations/de.json +++ b/custom_components/eufy_security/translations/de.json @@ -21,7 +21,8 @@ "auth": "Host/Port ist falsch." }, "abort": { - "single_instance_allowed": "Es ist nur eine einzige Instanz zulässig." + "single_instance_allowed": "Es ist nur eine einzige Instanz zulässig.", + "reauth_successful": "Code is received, reloading the integration in background automatically!" } }, "options": { diff --git a/custom_components/eufy_security/translations/en.json b/custom_components/eufy_security/translations/en.json index ba13d5a..8d35aaa 100644 --- a/custom_components/eufy_security/translations/en.json +++ b/custom_components/eufy_security/translations/en.json @@ -21,7 +21,8 @@ "auth": "Host/Port is wrong." }, "abort": { - "single_instance_allowed": "Only a single instance is allowed." + "single_instance_allowed": "Only a single instance is allowed.", + "reauth_successful": "Code is received, reloading the integration in background automatically!" } }, "options": { diff --git a/custom_components/eufy_security/translations/fr.json b/custom_components/eufy_security/translations/fr.json index 1b67243..cf20bf4 100644 --- a/custom_components/eufy_security/translations/fr.json +++ b/custom_components/eufy_security/translations/fr.json @@ -21,7 +21,8 @@ "auth": "L'hôte/le port est incorrect." }, "abort": { - "single_instance_allowed": "Une seule instance est autorisée." + "single_instance_allowed": "Une seule instance est autorisée.", + "reauth_successful": "Code is received, reloading the integration in background automatically!" } }, "options": { diff --git a/custom_components/eufy_security/translations/it.json b/custom_components/eufy_security/translations/it.json index 9e0d8e7..c60c32d 100644 --- a/custom_components/eufy_security/translations/it.json +++ b/custom_components/eufy_security/translations/it.json @@ -21,7 +21,8 @@ "auth": "L'host/porta è sbagliato." }, "abort": { - "single_instance_allowed": "È consentita una sola istanza." + "single_instance_allowed": "È consentita una sola istanza.", + "reauth_successful": "Code is received, reloading the integration in background automatically!" } }, "options": { diff --git a/custom_components/eufy_security/translations/nl.json b/custom_components/eufy_security/translations/nl.json index 35fac62..ebc483e 100644 --- a/custom_components/eufy_security/translations/nl.json +++ b/custom_components/eufy_security/translations/nl.json @@ -21,7 +21,8 @@ "auth": "Host/Poort klopt niet." }, "abort": { - "single_instance_allowed": "Slechts één exemplaar is toegestaan." + "single_instance_allowed": "Slechts één exemplaar is toegestaan.", + "reauth_successful": "Code is received, reloading the integration in background automatically!" } }, "options": { diff --git a/custom_components/eufy_security/translations/pl.json b/custom_components/eufy_security/translations/pl.json index 24863c9..73e511e 100644 --- a/custom_components/eufy_security/translations/pl.json +++ b/custom_components/eufy_security/translations/pl.json @@ -21,7 +21,8 @@ "auth": "Host lub Port jest nieprawidłowy." }, "abort": { - "single_instance_allowed": "Dozwolona jest tylko jedna instancja." + "single_instance_allowed": "Dozwolona jest tylko jedna instancja.", + "reauth_successful": "Code is received, reloading the integration in background automatically!" } }, "options": { diff --git a/custom_components/eufy_security/translations/pt-BR.json b/custom_components/eufy_security/translations/pt-BR.json index ac7f24e..875691c 100644 --- a/custom_components/eufy_security/translations/pt-BR.json +++ b/custom_components/eufy_security/translations/pt-BR.json @@ -21,7 +21,8 @@ "auth": "O host/porta está errado." }, "abort": { - "single_instance_allowed": "Apenas uma única instância é permitida." + "single_instance_allowed": "Apenas uma única instância é permitida.", + "reauth_successful": "Code is received, reloading the integration in background automatically!" } }, "options": {