Skip to content

Commit

Permalink
feat: recover broken connection to add-on
Browse files Browse the repository at this point in the history
  • Loading branch information
fuatakgun committed Oct 17, 2023
1 parent 5e08089 commit 3bca9d3
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 15 deletions.
15 changes: 9 additions & 6 deletions custom_components/eufy_security/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ async def handle_log_level(call):

return True


async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""setup config entry"""
if hass.data.get(DOMAIN) is None:
Expand All @@ -54,29 +53,33 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):

async def update(event_time_utc):
await coordinator.async_refresh()

async_track_time_interval(hass, update, timedelta(seconds=coordinator.config.sync_interval))

config_entry.add_update_listener(async_reload_entry)
return True
async_track_time_interval(hass, update, timedelta(seconds=coordinator.config.sync_interval))

return True

async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""unload active entities"""
_LOGGER.debug(f"async_unload_entry 1")
coordinator = hass.data[DOMAIN][COORDINATOR]
unloaded = all(
await asyncio.gather(
*[hass.config_entries.async_forward_entry_unload(config_entry, platform) for platform in coordinator.platforms]
)
)

if unloaded:
await coordinator.disconnect()
hass.data[DOMAIN] = {}

_LOGGER.debug(f"async_unload_entry 2")
return unloaded


async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""reload integration"""
_LOGGER.debug(f"async_reload_entry 1")
await async_unload_entry(hass, config_entry)
_LOGGER.debug(f"async_reload_entry 2")
await async_setup_entry(hass, config_entry)
_LOGGER.debug(f"async_reload_entry 3")

8 changes: 4 additions & 4 deletions custom_components/eufy_security/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,6 @@ async def handle_async_mjpeg_stream(self, request):
finally:
await stream.close()

@property
def available(self) -> bool:
return True

async def async_create_stream(self):
if self.coordinator.config.no_stream_in_hass is True:
return None
Expand All @@ -129,6 +125,10 @@ def is_streaming(self) -> bool:
"""Return true if the device is recording."""
return self.product.stream_status == StreamStatus.STREAMING

@property
def available(self) -> bool:
return True

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)
Expand Down
1 change: 1 addition & 0 deletions custom_components/eufy_security/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
DOMAIN = "eufy_security"
VERSION = "1.0.0"
COORDINATOR = "coordinator"
DISCONNECTED = "eufy-security-ws-disconnected"

PLATFORMS: list[str] = [
Platform.BINARY_SENSOR,
Expand Down
19 changes: 17 additions & 2 deletions custom_components/eufy_security/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""Module to initialize coordinator"""
import asyncio
from datetime import timedelta
import logging
import json
import asyncio
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN
from .const import DOMAIN, DISCONNECTED
from .eufy_security_api.api_client import ApiClient
from .eufy_security_api.exceptions import (
CaptchaRequiredException,
Expand Down Expand Up @@ -89,6 +91,19 @@ async def _update_local(self):
async def disconnect(self):
"""disconnect from api"""
await self._api.disconnect()
self._api = None

async def _async_reload(self, _):
await asyncio.sleep(5)
await self.hass.config_entries.async_reload(self.config.entry.entry_id)

def _on_error(self, error):
self.hass.components.persistent_notification.create(f"Error: {error}", title="Eufy Security - Error", notification_id="eufy_security_error")
"""raise notification on frontend when exception happens"""
self.hass.components.persistent_notification.create(f"Connection to Eufy Security add-on is broken, retrying in background!", title="Eufy Security - Error", notification_id="eufy_security_error")
self.hass.bus.async_listen_once(DISCONNECTED, self._async_reload)
self.hass.bus.async_fire(DISCONNECTED, None)

@property
def available(self) -> bool:
return self._api.available

6 changes: 4 additions & 2 deletions custom_components/eufy_security/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ def __init__(self, coordinator: EufySecurityDataUpdateCoordinator, metadata: Met
super().__init__(coordinator)
self.metadata: Metadata = metadata
self.product.set_state_update_listener(coordinator.async_update_listeners)
# self.product.set_state_update_listener(self.async_write_ha_state)
# platform = entity_platform.async_get_current_platform().domain
self._attr_unique_id = f"{DOMAIN}_{self.product.serial_no}_{self.product.product_type.value}_{metadata.name}"
self._attr_should_poll = False
self._attr_icon = self.description.icon
Expand All @@ -46,3 +44,7 @@ def description(self) -> PropertyToEntityDescription:
@property
def device_info(self):
return get_device_info(self.product)

@property
def available(self) -> bool:
return self.coordinator.available
11 changes: 10 additions & 1 deletion custom_components/eufy_security/eufy_security_api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,8 @@ async def _on_open(self) -> None:

def _on_close(self, future="") -> None:
_LOGGER.debug(f"on_close - executed - {future} = {future.exception()}")
self._on_error_callback(future)
if self._on_error_callback is not None:
self._on_error_callback(future)
if future.exception() is not None:
_LOGGER.debug(f"on_close - executed - {future.exception()}")
raise future.exception()
Expand All @@ -331,7 +332,15 @@ async def send_message(self, message: dict) -> None:

async def disconnect(self):
"""Disconnect the web socket and destroy it"""
self._on_error_callback = None
await self._client.disconnect()
self._client = None

@property
def available(self) -> bool:
return self._client.available




class IncomingMessageType(Enum):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async def _on_error(self, error: Text = "Unspecified") -> None:

def _on_close(self, future="") -> None:
self.socket = None
_LOGGER.debug(f"websocket client _on_close {self.socket is not None}")
if self.close_callback is not None:
self.close_callback(future)

Expand All @@ -84,3 +85,7 @@ async def send_message(self, message):
if self.socket is None:
raise WebSocketConnectionException("Connection to add-on was broken. please reload the integration!")
await self.socket.send_str(message)

@property
def available(self) -> bool:
return self.socket is not None
2 changes: 2 additions & 0 deletions custom_components/eufy_security/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ConfigField(Enum):
class Config:
"""Integration config options"""

entry: ConfigEntry = None
host: str = ConfigField.host.value
port: int = ConfigField.port.value
sync_interval: int = ConfigField.sync_interval.value
Expand All @@ -64,6 +65,7 @@ def parse(cls, config_entry: ConfigEntry):
"""Generate config instance from config entry"""
data_keys = ["host", "port"]
config = cls()
config.entry = config_entry
for key in config.__dict__:
if key in data_keys:
if config_entry.data.get(key, None) is not None:
Expand Down

0 comments on commit 3bca9d3

Please sign in to comment.