9
9
from urllib .parse import unquote
10
10
11
11
from aiohttp import ClientSession
12
- from aiohttp .web_exceptions import HTTPForbidden
13
- from retry import retry
12
+ from aiohttp .client_exceptions import ServerDisconnectedError
13
+ from aiohttp .web_exceptions import HTTPError , HTTPForbidden
14
+ from tenacity import (
15
+ before_sleep_log ,
16
+ retry ,
17
+ retry_if_exception_type ,
18
+ stop_after_attempt ,
19
+ wait_fixed ,
20
+ )
14
21
15
22
from .discovery import get_name
16
23
from .power import ATTR_COOL , ATTR_HEAT , ATTR_TOTAL , TIME_TODAY , DaikinPowerMixin
@@ -88,10 +95,10 @@ def discover_ip(device_id):
88
95
device_ip = device_name ['ip' ]
89
96
return device_id
90
97
91
- def __init__ (self , device_id , session : Optional [ClientSession ] = None ):
98
+ def __init__ (self , device_id , session : Optional [ClientSession ] = None ) -> None :
92
99
"""Init the pydaikin appliance, representing one Daikin device."""
93
100
self .values = ApplianceValues ()
94
- self .session = session
101
+ self .session = session if session is not None else ClientSession ()
95
102
self ._energy_consumption_history = defaultdict (list )
96
103
if session :
97
104
self .device_ip = device_id
@@ -113,25 +120,39 @@ async def init(self):
113
120
# Re-defined in all sub-classes
114
121
raise NotImplementedError
115
122
116
- @retry (tries = 3 , delay = 1 )
123
+ @retry (
124
+ reraise = True ,
125
+ wait = wait_fixed (1 ),
126
+ stop = stop_after_attempt (3 ),
127
+ retry = retry_if_exception_type (ServerDisconnectedError ),
128
+ before_sleep = before_sleep_log (_LOGGER , logging .DEBUG ),
129
+ )
117
130
async def _get_resource (self , path : str , params : Optional [dict ] = None ):
118
131
"""Make the http request."""
119
132
if params is None :
120
133
params = {}
121
134
122
- if self .session is None :
123
- session = ClientSession ()
124
- else :
125
- session = self .session
135
+ _LOGGER .debug ("Calling: %s/%s %s" , self .base_url , path , params )
126
136
127
- async with session as client_session , self .request_semaphore :
128
- async with client_session .get (
137
+ # cannot manage session on outer async with or this will close the session
138
+ # passed to pydaikin (homeassistant for instance)
139
+ async with self .request_semaphore :
140
+ async with self .session .get (
129
141
f'{ self .base_url } /{ path } ' , params = params
130
- ) as resp :
131
- if resp .status == 403 :
132
- raise HTTPForbidden
133
- assert resp .status == 200 , f"Response code is { resp .status } "
134
- return self .parse_response (await resp .text ())
142
+ ) as response :
143
+ if response .status == 403 :
144
+ raise HTTPForbidden (reason = f"HTTP 403 Forbidden for { response .url } " )
145
+ # Airbase returns a 404 response on invalid urls but requires fallback
146
+ if response .status == 404 :
147
+ _LOGGER .debug ("HTTP 404 Not Found for %s" , response .url )
148
+ return (
149
+ {}
150
+ ) # return an empty dict to indicate successful connection but bad data
151
+ if response .status != 200 :
152
+ raise HTTPError (
153
+ reason = f"Unexpected HTTP status code { response .status } for { response .url } "
154
+ )
155
+ return self .parse_response (await response .text ())
135
156
136
157
async def update_status (self , resources = None ):
137
158
"""Update status from resources."""
@@ -143,10 +164,16 @@ async def update_status(self, resources=None):
143
164
if self .values .should_resource_be_updated (resource )
144
165
]
145
166
_LOGGER .debug ("Updating %s" , resources )
146
- async with asyncio .TaskGroup () as tg :
147
- tasks = [
148
- tg .create_task (self ._get_resource (resource )) for resource in resources
149
- ]
167
+
168
+ try :
169
+ async with asyncio .TaskGroup () as tg :
170
+ tasks = [
171
+ tg .create_task (self ._get_resource (resource ))
172
+ for resource in resources
173
+ ]
174
+ except ExceptionGroup as eg :
175
+ for exc in eg .exceptions :
176
+ _LOGGER .error ("Exception in TaskGroup: %s" , exc )
150
177
151
178
for resource , task in zip (resources , tasks ):
152
179
self .values .update_by_resource (resource , task .result ())
@@ -175,6 +202,8 @@ def log_sensors(self, file):
175
202
data .append (('out_temp' , self .outside_temperature ))
176
203
if self .support_compressor_frequency :
177
204
data .append (('cmp_freq' , self .compressor_frequency ))
205
+ if self .support_filter_dirty :
206
+ data .append (('en_filter_sign' , self .filter_dirty ))
178
207
if self .support_energy_consumption :
179
208
data .append (
180
209
('total_today' , self .energy_consumption (ATTR_TOTAL , TIME_TODAY ))
@@ -201,6 +230,8 @@ def show_sensors(self):
201
230
data .append (f'out_temp={ int (self .outside_temperature )} °C' )
202
231
if self .support_compressor_frequency :
203
232
data .append (f'cmp_freq={ int (self .compressor_frequency )} Hz' )
233
+ if self .support_filter_dirty :
234
+ data .append (f'en_filter_sign={ int (self .filter_dirty )} ' )
204
235
if self .support_energy_consumption :
205
236
data .append (
206
237
f'total_today={ self .energy_consumption (ATTR_TOTAL , TIME_TODAY ):.01f} kWh'
@@ -281,6 +312,20 @@ def support_compressor_frequency(self) -> bool:
281
312
"""Return True if the device supports compressor frequency."""
282
313
return 'cmpfreq' in self .values
283
314
315
+ @property
316
+ def support_filter_dirty (self ) -> bool :
317
+ """Return True if the device supports dirty filter notification and it is turned on."""
318
+ return (
319
+ 'en_filter_sign' in self .values
320
+ and 'filter_sign_info' in self .values
321
+ and int (self ._parse_number ('en_filter_sign' )) == 1
322
+ )
323
+
324
+ @property
325
+ def support_zone_count (self ) -> bool :
326
+ """Return True if the device supports count of active zones."""
327
+ return 'en_zone' in self .values
328
+
284
329
@property
285
330
def support_energy_consumption (self ) -> bool :
286
331
"""Return True if the device supports energy consumption monitoring."""
@@ -306,6 +351,16 @@ def compressor_frequency(self) -> Optional[float]:
306
351
"""Return current compressor frequency."""
307
352
return self ._parse_number ('cmpfreq' )
308
353
354
+ @property
355
+ def filter_dirty (self ) -> Optional [float ]:
356
+ """Return current status of the filter."""
357
+ return self ._parse_number ('filter_sign_info' )
358
+
359
+ @property
360
+ def zone_count (self ) -> Optional [float ]:
361
+ """Return number of enabled zones."""
362
+ return self ._parse_number ('en_zone' )
363
+
309
364
@property
310
365
def humidity (self ) -> Optional [float ]:
311
366
"""Return current humidity."""
0 commit comments