Skip to content

Commit

Permalink
Auto reconnect monitor when fails
Browse files Browse the repository at this point in the history
  • Loading branch information
elad-bar committed Sep 18, 2022
1 parent 1993d8c commit c585e9f
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 24 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 1.3.2

- Add auto reconnect monitor when fails
- Add "_" to make private functions of Shinobi API - private

## 1.3.1

- Upgraded pre-commit to support Python 3.10
Expand Down
97 changes: 74 additions & 23 deletions custom_components/shinobi/component/api/shinobi_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from asyncio import sleep
from datetime import datetime
import json
import logging
Expand Down Expand Up @@ -36,6 +37,7 @@ class ShinobiApi:
base_url: str | None
monitors: dict[str, MonitorData]
status: ConnectivityStatus
repairing: list[str]

def __init__(self, hass: HomeAssistant, config_data: ConfigData):
try:
Expand All @@ -49,6 +51,7 @@ def __init__(self, hass: HomeAssistant, config_data: ConfigData):
self.session = None
self.base_url = None
self.monitors = {}
self.repairing = []
self.status = ConnectivityStatus.NotConnected

except Exception as ex:
Expand All @@ -60,7 +63,7 @@ def __init__(self, hass: HomeAssistant, config_data: ConfigData):
)

@property
def api_url(self):
def _api_url(self):
config_data = self.config_data
protocol = PROTOCOLS[config_data.ssl]

Expand All @@ -82,7 +85,7 @@ async def initialize(self, config_data: ConfigData | None = None):
if config_data is not None:
self.config_data = config_data

self.base_url = self.api_url
self.base_url = self._api_url
self.video_list = []

if self.hass is None:
Expand All @@ -107,13 +110,13 @@ def build_url(self, endpoint, monitor_id: str = None):
if endpoint.startswith("/"):
endpoint = endpoint[1:]

endpoint = self.build_endpoint(endpoint, monitor_id)
endpoint = self._build_endpoint(endpoint, monitor_id)

url = f"{self.base_url}{endpoint}"

return url

def build_endpoint(self, endpoint, monitor_id: str = None):
def _build_endpoint(self, endpoint, monitor_id: str = None):
if endpoint.startswith("/"):
endpoint = endpoint[1:]

Expand All @@ -132,11 +135,11 @@ def _validate_request(self, endpoint):
if not ConnectivityStatus.is_api_request_allowed(endpoint, self.status):
raise APIValidationException(endpoint, self.status)

async def async_post(self,
endpoint,
request_data: dict,
monitor_id: str | None = None,
is_url_encoded: bool = False):
async def _async_post(self,
endpoint,
request_data: dict,
monitor_id: str | None = None,
is_url_encoded: bool = False):
result = None

try:
Expand Down Expand Up @@ -178,7 +181,7 @@ async def async_post(self,

return result

async def async_get(self, endpoint, resource_available_check: bool = False):
async def _async_get(self, endpoint, resource_available_check: bool = False):
result = None

try:
Expand Down Expand Up @@ -228,8 +231,14 @@ async def async_update(self):
await self.initialize()

if self.status == ConnectivityStatus.Connected:
await self.load_monitors()
await self.load_videos()
await self._load_monitors()
await self._load_videos()

for monitor_id in self.monitors:
monitor = self.monitors.get(monitor_id)

if monitor_id not in self.repairing and monitor.should_repair:
self.hass.async_create_task(self._async_repair_monitor(monitor_id))

async def _login(self):
_LOGGER.info("Performing login")
Expand All @@ -247,7 +256,7 @@ async def _login(self):
LOGIN_PASSWORD: config_data.password
}

login_data = await self.async_post(URL_LOGIN, data)
login_data = await self._async_post(URL_LOGIN, data)

if login_data is not None:
user_data = login_data.get("$user", {})
Expand All @@ -262,7 +271,7 @@ async def _login(self):
self.api_key = temp_api_key
self.status = ConnectivityStatus.TemporaryConnected

api_keys_data: dict = await self.async_get(URL_API_KEYS)
api_keys_data: dict = await self._async_get(URL_API_KEYS)

self.api_key = None

Expand Down Expand Up @@ -305,17 +314,17 @@ async def get_socket_io_version(self):
_LOGGER.debug("Get SocketIO version")
version = 3

response: bool = await self.async_get(URL_SOCKET_IO_V4, True)
response: bool = await self._async_get(URL_SOCKET_IO_V4, True)

if response:
version = 4

return version

async def load_monitors(self):
async def _load_monitors(self):
_LOGGER.debug("Retrieving monitors")

response: dict = await self.async_get(URL_MONITORS)
response: dict = await self._async_get(URL_MONITORS)

if response is None:
_LOGGER.warning("No monitors were found")
Expand Down Expand Up @@ -350,11 +359,11 @@ async def load_monitors(self):
f"Failed to load monitor data: {monitor}, Error: {ex}, Line: {line_number}"
)

async def load_videos(self):
async def _load_videos(self):
_LOGGER.debug("Retrieving videos list")
video_list = []

response: dict = await self.async_get(URL_VIDEOS)
response: dict = await self._async_get(URL_VIDEOS)

if response is None:
_LOGGER.warning("Invalid video response")
Expand Down Expand Up @@ -387,12 +396,54 @@ async def load_videos(self):

self.video_list = video_list

async def _async_repair_monitor(self, monitor_id: str):
monitor = self.monitors.get(monitor_id)

if monitor_id in self.repairing:
_LOGGER.warning(f"Monitor {monitor_id} is in progress, cannot start additional repair job")

elif not monitor.should_repair:
_LOGGER.warning(f"Monitor {monitor_id} is working properly, no need to repair")

else:
try:
_LOGGER.info(f"Repairing monitor {monitor_id}")

self.repairing.append(monitor_id)

await self.async_set_monitor_mode(monitor_id, MONITOR_MODE_STOP)

await sleep(5)

await self.async_set_monitor_mode(monitor_id, MONITOR_MODE_RECORD)

await sleep(15)

monitor = self.monitors.get(monitor_id)

if monitor.should_repair:
_LOGGER.warning(f"Unable to repair monitor: {monitor_id}")

else:
_LOGGER.info(f"Monitor {monitor_id} is repaired")

except Exception as ex:
exc_type, exc_obj, tb = sys.exc_info()
line_number = tb.tb_lineno

_LOGGER.error(
f"Failed to repair monitor: {monitor_id}, Error: {ex}, Line: {line_number}"
)

finally:
self.repairing.remove(monitor_id)

async def async_set_monitor_mode(self, monitor_id: str, mode: str):
_LOGGER.info(f"Updating monitor {monitor_id} mode to {mode}")

endpoint = self.build_endpoint(f"{URL_UPDATE_MODE}/{mode}", monitor_id)
endpoint = self._build_endpoint(f"{URL_UPDATE_MODE}/{mode}", monitor_id)

response = await self.async_get(endpoint)
response = await self._async_get(endpoint)

response_message = response.get("msg")

Expand All @@ -416,7 +467,7 @@ async def _async_set_detection_mode(self, monitor_id: str, detector: str, enable

url = f"{URL_MONITORS}/{monitor_id}"

response: dict = await self.async_get(url)
response: dict = await self._async_get(url)
monitor_data = response[0]
monitor_details_str = monitor_data.get(ATTR_MONITOR_DETAILS)
details = json.loads(monitor_details_str)
Expand All @@ -429,7 +480,7 @@ async def _async_set_detection_mode(self, monitor_id: str, detector: str, enable
"data": monitor_data
}

response = await self.async_post(URL_UPDATE_MONITOR, data, monitor_id, True)
response = await self._async_post(URL_UPDATE_MONITOR, data, monitor_id, True)

response_message = response.get("msg")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ def _get_monitor_device_name(self, monitor: MonitorData):

return device_name

async def async_repair_monitor(self, monitor_id: str):
await self.api.async_repair_monitor(monitor_id)

async def async_set_monitor_mode(self, monitor_id: str, mode: str):
await self.api.async_set_monitor_mode(monitor_id, mode)

Expand Down
8 changes: 8 additions & 0 deletions custom_components/shinobi/component/models/monitor_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class MonitorData:
jpeg_api_enabled: bool
original_stream: str
mode: str
status: str

def __init__(self, monitor):
try:
Expand Down Expand Up @@ -64,6 +65,13 @@ def disabled(self):

return is_disabled

@property
def should_repair(self):
is_mode_recording = self.mode == MONITOR_MODE_RECORD
is_status_recording = self.status.lower().startswith(MONITOR_MODE_RECORD)

return is_mode_recording and not is_status_recording

def is_detector_active(self, sensor_type: BinarySensorDeviceClass):
result = False

Expand Down
2 changes: 1 addition & 1 deletion custom_components/shinobi/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"codeowners": ["@elad-bar"],
"requirements": [ ],
"config_flow": true,
"version": "1.3.1",
"version": "1.3.2",
"iot_class": "local_polling"
}

0 comments on commit c585e9f

Please sign in to comment.