Skip to content

Commit c10ab11

Browse files
authored
Merge pull request #6 from kingy444/merge-bitbucket
Fix HomeAssistant capability and add Features for AirBase
2 parents 47d67a3 + cbdd1a8 commit c10ab11

File tree

9 files changed

+118
-36
lines changed

9 files changed

+118
-36
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ repos:
2929
- aiohttp==3.7.3
3030
- netifaces==0.11.0
3131
- urllib3==1.26.3
32-
- retry==0.9.2
32+
- tenacity==8.2.3
3333
exclude: 'tests/'
3434
args:
3535
- --ignore=setup.py
36+
- --extension-pkg-allow-list=netifaces

pydaikin/daikin_airbase.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ def parse_response(response_body):
5757

5858
return response
5959

60-
def __init__(
60+
def __init__( # pylint:disable=useless-parent-delegation
6161
self, device_id, session=None
62-
): # pylint:disable=useless-super-delegation
62+
) -> None:
6363
"""Init the pydaikin appliance, representing one Daikin AirBase
6464
(BRP15B61) device."""
6565
super().__init__(device_id, session)
@@ -70,6 +70,9 @@ async def init(self):
7070
if not self.values:
7171
raise DaikinException("Empty values.")
7272
self.values.update({**self.DEFAULTS, **self.values})
73+
# Friendly display the model
74+
if self.values.get("model", None) == "NOTSUPPORT":
75+
self.values["model"] = "Airbase BRP15B61"
7376

7477
async def _get_resource(self, path: str, params: Optional[dict] = None):
7578
"""Make the http request."""
@@ -87,9 +90,15 @@ def support_swing_mode(self):
8790
return False
8891

8992
@property
90-
def support_outside_temperature(self):
91-
"""AirBase unit returns otemp if master controller starts before it."""
92-
return True
93+
def outside_temperature(self):
94+
"""
95+
AirBase unit returns otemp if master controller starts before it.
96+
97+
No Outside Thermometor returns a '-' (Non Number).
98+
Return current outside temperature if available.
99+
"""
100+
value = self.values.get('otemp')
101+
return self._parse_number('otemp') if value != '-' else None
93102

94103
@property
95104
def support_zone_temperature(self):
@@ -174,8 +183,13 @@ def zones(self):
174183
"""Return list of zones."""
175184
if not self.values.get("zone_name"):
176185
return None
186+
enabled_zones = len(self.represent("zone_name")[1])
187+
if self.support_zone_count:
188+
enabled_zones = int(self.zone_count) # float to int
177189
zone_onoff = self.represent("zone_onoff")[1]
178-
zone_list = self.represent("zone_name")[1]
190+
zone_list = self.represent("zone_name")[1][
191+
:enabled_zones
192+
] # Slicing to limit zones
179193
if self.support_zone_temperature:
180194
mode = self.values["mode"]
181195

pydaikin/daikin_base.py

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@
99
from urllib.parse import unquote
1010

1111
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+
)
1421

1522
from .discovery import get_name
1623
from .power import ATTR_COOL, ATTR_HEAT, ATTR_TOTAL, TIME_TODAY, DaikinPowerMixin
@@ -88,10 +95,10 @@ def discover_ip(device_id):
8895
device_ip = device_name['ip']
8996
return device_id
9097

91-
def __init__(self, device_id, session: Optional[ClientSession] = None):
98+
def __init__(self, device_id, session: Optional[ClientSession] = None) -> None:
9299
"""Init the pydaikin appliance, representing one Daikin device."""
93100
self.values = ApplianceValues()
94-
self.session = session
101+
self.session = session if session is not None else ClientSession()
95102
self._energy_consumption_history = defaultdict(list)
96103
if session:
97104
self.device_ip = device_id
@@ -113,25 +120,39 @@ async def init(self):
113120
# Re-defined in all sub-classes
114121
raise NotImplementedError
115122

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+
)
117130
async def _get_resource(self, path: str, params: Optional[dict] = None):
118131
"""Make the http request."""
119132
if params is None:
120133
params = {}
121134

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)
126136

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(
129141
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())
135156

136157
async def update_status(self, resources=None):
137158
"""Update status from resources."""
@@ -143,10 +164,16 @@ async def update_status(self, resources=None):
143164
if self.values.should_resource_be_updated(resource)
144165
]
145166
_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)
150177

151178
for resource, task in zip(resources, tasks):
152179
self.values.update_by_resource(resource, task.result())
@@ -175,6 +202,8 @@ def log_sensors(self, file):
175202
data.append(('out_temp', self.outside_temperature))
176203
if self.support_compressor_frequency:
177204
data.append(('cmp_freq', self.compressor_frequency))
205+
if self.support_filter_dirty:
206+
data.append(('en_filter_sign', self.filter_dirty))
178207
if self.support_energy_consumption:
179208
data.append(
180209
('total_today', self.energy_consumption(ATTR_TOTAL, TIME_TODAY))
@@ -201,6 +230,8 @@ def show_sensors(self):
201230
data.append(f'out_temp={int(self.outside_temperature)}°C')
202231
if self.support_compressor_frequency:
203232
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)}')
204235
if self.support_energy_consumption:
205236
data.append(
206237
f'total_today={self.energy_consumption(ATTR_TOTAL, TIME_TODAY):.01f}kWh'
@@ -281,6 +312,20 @@ def support_compressor_frequency(self) -> bool:
281312
"""Return True if the device supports compressor frequency."""
282313
return 'cmpfreq' in self.values
283314

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+
284329
@property
285330
def support_energy_consumption(self) -> bool:
286331
"""Return True if the device supports energy consumption monitoring."""
@@ -306,6 +351,16 @@ def compressor_frequency(self) -> Optional[float]:
306351
"""Return current compressor frequency."""
307352
return self._parse_number('cmpfreq')
308353

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+
309364
@property
310365
def humidity(self) -> Optional[float]:
311366
"""Return current humidity."""

pydaikin/daikin_brp069.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class DaikinBRP069(Appliance):
114114
'en_hol': 'away_mode',
115115
'cur': 'internal clock',
116116
'adv': 'advanced mode',
117+
'filter_sign_info': 'filter dirty',
117118
}
118119

119120
async def init(self):

pydaikin/daikin_brp072c.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
class DaikinBRP072C(DaikinBRP069):
1313
"""Daikin class for BRP072Cxx units."""
1414

15-
def __init__(self, device_id, session=None, key=None, uuid=None):
15+
def __init__(self, device_id, session=None, key=None, uuid=None) -> None:
1616
"""Init the pydaikin appliance, representing one Daikin AirBase
1717
(BRP15B61) device."""
1818
super().__init__(device_id, session)

pydaikin/daikin_skyfi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class DaikinSkyFi(Appliance):
4747
},
4848
}
4949

50-
def __init__(self, device_id, session=None, password=None):
50+
def __init__(self, device_id, session=None, password=None) -> None:
5151
"""Init the pydaikin appliance, representing one Daikin SkyFi device."""
5252
super().__init__(device_id, session)
5353
self.device_ip = f'{self.device_ip}:2000'

pydaikin/discovery.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
class Discovery: # pylint: disable=too-few-public-methods
2222
"""Discovery class."""
2323

24-
def __init__(self):
24+
def __init__(self) -> None:
2525
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
2626
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
2727
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

pydaikin/factory.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"Factory to generate Pydaikin complete objects"
22

3+
import logging
34
from typing import Optional
45

56
from aiohttp import ClientSession
7+
from aiohttp.web_exceptions import HTTPNotFound
68

79
from .daikin_airbase import DaikinAirBase
810
from .daikin_base import Appliance
@@ -11,6 +13,8 @@
1113
from .daikin_skyfi import DaikinSkyFi
1214
from .exceptions import DaikinException
1315

16+
_LOGGER = logging.getLogger(__name__)
17+
1418

1519
class DaikinFactory: # pylint: disable=too-few-public-methods
1620
"Factory object generating instantiated instances of Appliance"
@@ -42,11 +46,16 @@ async def __init__(
4246
uuid=kwargs.get('uuid'),
4347
)
4448
else: # special case for BRP069 and AirBase
45-
self._generated_object = DaikinBRP069(device_id, session)
46-
await self._generated_object.update_status(
47-
self._generated_object.HTTP_RESOURCES[:1]
48-
)
49-
if not self._generated_object.values:
49+
try:
50+
_LOGGER.debug("Trying connection to BRP069")
51+
self._generated_object = DaikinBRP069(device_id, session)
52+
await self._generated_object.update_status(
53+
self._generated_object.HTTP_RESOURCES[:1]
54+
)
55+
if not self._generated_object.values:
56+
raise DaikinException("Empty Values.")
57+
except (HTTPNotFound, DaikinException) as err:
58+
_LOGGER.debug("Falling back to AirBase: %s", err)
5059
self._generated_object = DaikinAirBase(device_id, session)
5160

5261
await self._generated_object.init()
@@ -55,3 +64,5 @@ async def __init__(
5564
raise DaikinException(
5665
f"Error creating device, {device_id} is not supported."
5766
)
67+
68+
_LOGGER.debug("Daikin generated object: %s", self._generated_object)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
netifaces
22
aiohttp
33
urllib3
4-
retry
4+
tenacity

0 commit comments

Comments
 (0)