33import json
44import logging
55import sys
6+ import asyncio
67from typing import Optional
78
89import aiohttp
9- from aiohttp import ClientSession
10-
10+ from aiohttp import ClientSession , ClientTimeout
1111from homeassistant .core import HomeAssistant
1212from homeassistant .helpers .aiohttp_client import async_create_clientsession
1313
1818
1919_LOGGER = logging .getLogger (__name__ )
2020
21+ DEFAULT_TIMEOUT = ClientTimeout (total = 10 )
22+ MAX_RETRIES = 3
23+ RETRY_DELAY = 1 # seconds
24+
2125
2226class 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