Skip to content

Commit cbd1ad7

Browse files
authored
Merge pull request #256 from garrywma/master
Update blue_iris_api.py
2 parents 7c6cec5 + 9a546d9 commit cbd1ad7

1 file changed

Lines changed: 83 additions & 178 deletions

File tree

custom_components/blueiris/api/blue_iris_api.py

Lines changed: 83 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import json
44
import logging
55
import sys
6+
import asyncio
67
from typing import Optional
78

89
import aiohttp
9-
from aiohttp import ClientSession
10-
10+
from aiohttp import ClientSession, ClientTimeout
1111
from homeassistant.core import HomeAssistant
1212
from homeassistant.helpers.aiohttp_client import async_create_clientsession
1313

@@ -18,13 +18,17 @@
1818

1919
_LOGGER = logging.getLogger(__name__)
2020

21+
DEFAULT_TIMEOUT = ClientTimeout(total=10)
22+
MAX_RETRIES = 3
23+
RETRY_DELAY = 1 # seconds
24+
2125

2226
class BlueIrisApi:
23-
"""The Class for handling the data retrieval."""
27+
"""Handles Blue Iris data retrieval and control."""
2428

2529
is_logged_in: bool
2630
session_id: Optional[str]
27-
session: ClientSession
31+
session: Optional[ClientSession]
2832
data: dict
2933
status: dict
3034
camera_list: list[CameraData]
@@ -39,14 +43,10 @@ def __init__(self, hass: HomeAssistant, config_manager: ConfigManager):
3943
self.hass = hass
4044
self.config_manager = config_manager
4145
self.session_id = None
42-
46+
self.session = None
4347
except Exception as ex:
4448
exc_type, exc_obj, tb = sys.exc_info()
45-
line_number = tb.tb_lineno
46-
47-
_LOGGER.error(
48-
f"Failed to load BlueIris API, error: {ex}, line: {line_number}"
49-
)
49+
_LOGGER.error(f"Failed to load BlueIris API, error: {ex}, line: {tb.tb_lineno}")
5050

5151
@property
5252
def is_initialized(self):
@@ -56,259 +56,164 @@ def is_initialized(self):
5656
def config_data(self):
5757
return self.config_manager.data
5858

59-
async def async_verified_post(self, data):
60-
result = None
61-
62-
for i in range(2):
63-
result = await self.async_post(data)
64-
65-
if result is not None:
66-
result_status = result.get("result")
67-
68-
if result_status == "fail":
69-
error_msg = f"Request #{i} to BlueIris ({self.base_url}) failed, Data: {data}, Response: {result}"
70-
71-
_LOGGER.warning(error_msg)
72-
73-
await self.login()
74-
75-
else:
76-
break
59+
async def ensure_session(self):
60+
if self.session is None or self.session.closed:
61+
if self.hass is None:
62+
self.session = aiohttp.ClientSession(timeout=DEFAULT_TIMEOUT)
63+
else:
64+
self.session = async_create_clientsession(hass=self.hass, timeout=DEFAULT_TIMEOUT)
7765

78-
return result
66+
async def async_close(self):
67+
if self.session and not self.session.closed:
68+
await self.session.close()
7969

8070
async def async_post(self, data):
71+
await self.ensure_session()
8172
result = None
8273

83-
try:
84-
async with self.session.post(
85-
self.url, data=json.dumps(data), ssl=False
86-
) as response:
87-
_LOGGER.debug(f"Status of {self.url}: {response.status}")
88-
89-
response.raise_for_status()
90-
91-
result = await response.json()
92-
93-
_LOGGER.debug(f"Full result of {data}: {result}")
74+
for attempt in range(MAX_RETRIES):
75+
try:
76+
async with self.session.post(self.url, data=json.dumps(data), ssl=False) as response:
77+
_LOGGER.debug(f"Status of {self.url}: {response.status}")
78+
response.raise_for_status()
79+
result = await response.json()
80+
_LOGGER.debug(f"Full result of {data}: {result}")
81+
self._last_update = datetime.now()
82+
return result
83+
except aiohttp.ClientError as ex:
84+
_LOGGER.warning(f"Attempt {attempt+1} failed: {ex}")
85+
await asyncio.sleep(RETRY_DELAY)
86+
except Exception as ex:
87+
exc_type, exc_obj, tb = sys.exc_info()
88+
_LOGGER.error(f"Unexpected error on attempt {attempt+1}, Error: {ex}, Line: {tb.tb_lineno}")
89+
await asyncio.sleep(RETRY_DELAY)
90+
91+
_LOGGER.error(f"All attempts to POST to {self.url} failed.")
92+
return None
9493

95-
self._last_update = datetime.now()
96-
97-
except Exception as ex:
98-
exc_type, exc_obj, tb = sys.exc_info()
99-
line_number = tb.tb_lineno
94+
async def async_verified_post(self, data):
95+
for i in range(2):
96+
result = await self.async_post(data)
97+
if result is not None and result.get("result") != "fail":
98+
return result
10099

101-
_LOGGER.error(
102-
f"Failed to connect {self.url}, Error: {ex}, Line: {line_number}"
103-
)
100+
_LOGGER.warning(f"Request #{i} to BlueIris ({self.base_url}) failed, Data: {data}, Response: {result}")
101+
await self.login()
104102

105-
return result
103+
return None
106104

107105
async def initialize(self):
108106
_LOGGER.debug("Initializing BlueIris")
109-
110107
try:
111108
config_data = self.config_data
112-
113-
self.base_url = (
114-
f"{config_data.protocol}://{config_data.host}:{config_data.port}"
115-
)
109+
self.base_url = f"{config_data.protocol}://{config_data.host}:{config_data.port}"
116110
self.url = f"{self.base_url}/json"
117111

118112
self.is_logged_in = False
119113
self.data = {}
120114
self.status = {}
121115
self.camera_list = []
122116

123-
if self.hass is None:
124-
if self.session is not None:
125-
await self.session.close()
126-
127-
self.session = aiohttp.client.ClientSession()
128-
else:
129-
self.session = async_create_clientsession(hass=self.hass)
130-
117+
await self.ensure_session()
131118
await self.login()
132-
133119
except Exception as ex:
134120
exc_type, exc_obj, tb = sys.exc_info()
135-
line_number = tb.tb_lineno
136-
137-
_LOGGER.error(
138-
f"Failed to initialize BlueIris API ({self.base_url}), error: {ex}, line: {line_number}"
139-
)
121+
_LOGGER.error(f"Failed to initialize BlueIris API ({self.base_url}), error: {ex}, line: {tb.tb_lineno}")
140122

141123
async def async_update(self):
142-
_LOGGER.debug(
143-
f"Updating data from BI Server ({self.config_manager.config_entry.title})"
144-
)
145-
124+
_LOGGER.debug(f"Updating data from BI Server ({self.config_manager.config_entry.title})")
146125
await self.load_camera()
147126
await self.load_status()
148127

149128
async def load_session_id(self):
150129
_LOGGER.debug("Retrieving session ID")
151-
152-
request_data = {"cmd": "login"}
153-
154-
response = await self.async_post(request_data)
155-
156-
self.session_id = None
157-
158-
if response is not None:
159-
self.session_id = response.get("session")
160-
130+
response = await self.async_post({"cmd": "login"})
131+
self.session_id = response.get("session") if response else None
161132
self.is_logged_in = False
162133

163134
async def login(self):
164135
_LOGGER.debug("Performing login")
165-
166-
logged_in = False
167-
168136
try:
169137
await self.load_session_id()
170-
171-
if self.session_id is not None:
138+
if self.session_id:
172139
config_data = self.config_manager.data
173-
username = config_data.username
174-
password = config_data.password_clear_text
140+
token_request = f"{config_data.username}:{self.session_id}:{config_data.password_clear_text}"
141+
token = hashlib.md5(token_request.encode("utf-8")).hexdigest()
175142

176-
token_request = f"{username}:{self.session_id}:{password}"
177-
m = hashlib.md5()
178-
m.update(token_request.encode("utf-8"))
179-
token = m.hexdigest()
180-
181-
request_data = {
143+
result = await self.async_post({
182144
"cmd": "login",
183145
"session": self.session_id,
184146
"response": token,
185-
}
186-
187-
result = await self.async_post(request_data)
188-
189-
if result is not None:
190-
result_status = result.get("result")
191-
192-
if result_status == "success":
193-
logged_in = True
194-
195-
data = result.get("data", {})
196-
197-
for key in data:
198-
self.data[key] = data[key]
147+
})
199148

149+
if result and result.get("result") == "success":
150+
self.is_logged_in = True
151+
self.data.update(result.get("data", {}))
200152
except Exception as ex:
201153
exc_type, exc_obj, tb = sys.exc_info()
202-
line_number = tb.tb_lineno
203-
204-
_LOGGER.error(f"Failed to login, Error: {ex}, Line: {line_number}")
205-
206-
self.is_logged_in = logged_in
154+
_LOGGER.error(f"Failed to login, Error: {ex}, Line: {tb.tb_lineno}")
207155

208156
return self.is_logged_in
209157

210158
async def load_camera(self):
211159
_LOGGER.debug("Retrieving camera list")
212-
213-
request_data = {"cmd": "camlist", "session": self.session_id}
214-
215-
response = await self.async_verified_post(request_data)
216-
217-
if response is not None:
218-
data = response.get("data", [])
219-
camera_items = []
220-
221-
for camera in data:
222-
camera_data = CameraData(camera)
223-
224-
camera_items.append(camera_data)
225-
226-
self.camera_list = camera_items
160+
response = await self.async_verified_post({"cmd": "camlist", "session": self.session_id})
161+
if response:
162+
self.camera_list = [CameraData(cam) for cam in response.get("data", [])]
227163

228164
async def load_status(self):
229165
_LOGGER.debug("Retrieving status")
230-
231-
request_data = {"cmd": "status", "session": self.session_id}
232-
233-
response = await self.async_verified_post(request_data)
234-
235-
if response is not None:
236-
data = response.get("data", {})
237-
238-
for key in data:
239-
self.status[key] = data[key]
166+
response = await self.async_verified_post({"cmd": "status", "session": self.session_id})
167+
if response:
168+
self.status.update(response.get("data", {}))
240169

241170
async def set_profile(self, profile_id):
242171
_LOGGER.debug(f"Setting profile {profile_id}")
243-
244172
await self._set_profile(profile_id)
245173

246174
async def _set_profile(self, profile_id, check_lock=True):
247-
request_data = {
175+
response = await self.async_verified_post({
248176
"cmd": "status",
249177
"session": self.session_id,
250178
"profile": profile_id,
251-
}
252-
253-
response = await self.async_verified_post(request_data)
254-
255-
if response is not None:
179+
})
180+
if response:
256181
data = response.get("data", {})
257-
258-
lock = data.get("lock")
259-
260-
if check_lock and lock != 1:
182+
if check_lock and data.get("lock") != 1:
261183
await self._set_profile(profile_id, False)
262-
263184
return
264-
265-
for key in data:
266-
self.status[key] = data[key]
185+
self.status.update(data)
267186

268187
async def set_schedule(self, schedule_name):
269188
_LOGGER.debug(f"Setting schedule {schedule_name}")
270-
271189
await self._set_schedule(schedule_name)
272190

273191
async def _set_schedule(self, schedule_name, check_lock=True):
274-
request_data = {
192+
response = await self.async_verified_post({
275193
"cmd": "status",
276194
"session": self.session_id,
277195
"schedule": schedule_name,
278-
}
279-
280-
response = await self.async_verified_post(request_data)
281-
282-
if response is not None:
196+
})
197+
if response:
283198
data = response.get("data", {})
284-
285-
lock = data.get("lock")
286-
287-
if check_lock and lock != 1:
199+
if check_lock and data.get("lock") != 1:
288200
await self._set_schedule(schedule_name, False)
289-
290201
return
291-
292-
for key in data:
293-
self.status[key] = data[key]
202+
self.status.update(data)
294203

295204
async def trigger_camera(self, camera_short_name):
296205
_LOGGER.debug(f"Triggering camera {camera_short_name}")
297-
298-
request_data = {
206+
await self.async_verified_post({
299207
"cmd": "trigger",
300208
"session": self.session_id,
301209
"camera": camera_short_name,
302-
}
303-
await self.async_verified_post(request_data)
210+
})
304211

305212
async def move_to_preset(self, camera_short_name, preset):
306213
_LOGGER.debug(f"Moving {camera_short_name} to preset {preset}")
307-
preset_value = 100 + preset
308-
request_data = {
214+
await self.async_verified_post({
309215
"cmd": "ptz",
310216
"session": self.session_id,
311217
"camera": camera_short_name,
312-
"button": preset_value,
313-
}
314-
await self.async_verified_post(request_data)
218+
"button": 100 + preset,
219+
})

0 commit comments

Comments
 (0)