Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix HomeAssistant capability and add Features #6

Merged
merged 21 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ repos:
- aiohttp==3.7.3
- netifaces==0.11.0
- urllib3==1.26.3
- retry==0.9.2
- tenacity==8.2.3
exclude: 'tests/'
args:
- --ignore=setup.py
fredrike marked this conversation as resolved.
Show resolved Hide resolved
24 changes: 19 additions & 5 deletions pydaikin/daikin_airbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def parse_response(response_body):

def __init__(
fredrike marked this conversation as resolved.
Show resolved Hide resolved
self, device_id, session=None
): # pylint:disable=useless-super-delegation
) -> None: # pylint:disable=useless-parent-delegation
fredrike marked this conversation as resolved.
Show resolved Hide resolved
"""Init the pydaikin appliance, representing one Daikin AirBase
(BRP15B61) device."""
super().__init__(device_id, session)
Expand All @@ -70,6 +70,9 @@ async def init(self):
if not self.values:
raise DaikinException("Empty values.")
self.values.update({**self.DEFAULTS, **self.values})
# Friendly display the model
if self.values.get("model", None) == "NOTSUPPORT":
self.values["model"] = "Airbase BRP15B61"

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

@property
def support_outside_temperature(self):
"""AirBase unit returns otemp if master controller starts before it."""
return True
def outside_temperature(self):
"""
AirBase unit returns otemp if master controller starts before it.

No Outside Thermometor returns a '-' (Non Number).
Return current outside temperature if available.
"""
value = self.values.get('otemp')
return self._parse_number('otemp') if value != '-' else None

@property
def support_zone_temperature(self):
Expand Down Expand Up @@ -174,8 +183,13 @@ def zones(self):
"""Return list of zones."""
if not self.values.get("zone_name"):
return None
enabled_zones = len(self.represent("zone_name")[1])
if self.support_zone_count:
enabled_zones = int(self.zone_count) # float to int
zone_onoff = self.represent("zone_onoff")[1]
zone_list = self.represent("zone_name")[1]
zone_list = self.represent("zone_name")[1][
:enabled_zones
] # Slicing to limit zones
if self.support_zone_temperature:
mode = self.values["mode"]

Expand Down
95 changes: 75 additions & 20 deletions pydaikin/daikin_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@
from urllib.parse import unquote

from aiohttp import ClientSession
from aiohttp.web_exceptions import HTTPForbidden
from retry import retry
from aiohttp.client_exceptions import ServerDisconnectedError
from aiohttp.web_exceptions import HTTPError, HTTPForbidden
from tenacity import (
before_sleep_log,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_fixed,
)

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

def __init__(self, device_id, session: Optional[ClientSession] = None):
def __init__(self, device_id, session: Optional[ClientSession] = None) -> None:
"""Init the pydaikin appliance, representing one Daikin device."""
self.values = ApplianceValues()
self.session = session
self.session = session if session is not None else ClientSession()
self._energy_consumption_history = defaultdict(list)
if session:
self.device_ip = device_id
Expand All @@ -113,25 +120,39 @@ async def init(self):
# Re-defined in all sub-classes
raise NotImplementedError

@retry(tries=3, delay=1)
@retry(
reraise=True,
wait=wait_fixed(1),
stop=stop_after_attempt(3),
retry=retry_if_exception_type(ServerDisconnectedError),
before_sleep=before_sleep_log(_LOGGER, logging.DEBUG),
)
async def _get_resource(self, path: str, params: Optional[dict] = None):
"""Make the http request."""
if params is None:
params = {}

if self.session is None:
session = ClientSession()
else:
session = self.session
_LOGGER.debug("Calling: %s/%s %s", self.base_url, path, params)

async with session as client_session, self.request_semaphore:
async with client_session.get(
# cannot manage session on outer async with or this will close the session
# passed to pydaikin (homeassistant for instance)
async with self.request_semaphore:
async with self.session.get(
f'{self.base_url}/{path}', params=params
) as resp:
if resp.status == 403:
raise HTTPForbidden
assert resp.status == 200, f"Response code is {resp.status}"
return self.parse_response(await resp.text())
) as response:
if response.status == 403:
raise HTTPForbidden(reason=f"HTTP 403 Forbidden for {response.url}")
# Airbase returns a 404 response on invalid urls but requires fallback
if response.status == 404:
_LOGGER.debug("HTTP 404 Not Found for %s", response.url)
return (
{}
) # return an empty dict to indicate successful connection but bad data
if response.status != 200:
raise HTTPError(
reason=f"Unexpected HTTP status code {response.status} for {response.url}"
)
return self.parse_response(await response.text())

async def update_status(self, resources=None):
"""Update status from resources."""
Expand All @@ -143,10 +164,16 @@ async def update_status(self, resources=None):
if self.values.should_resource_be_updated(resource)
]
_LOGGER.debug("Updating %s", resources)
async with asyncio.TaskGroup() as tg:
tasks = [
tg.create_task(self._get_resource(resource)) for resource in resources
]

try:
async with asyncio.TaskGroup() as tg:
tasks = [
tg.create_task(self._get_resource(resource))
for resource in resources
]
except ExceptionGroup as eg:
for exc in eg.exceptions:
_LOGGER.error("Exception in TaskGroup: %s", exc)

for resource, task in zip(resources, tasks):
self.values.update_by_resource(resource, task.result())
Expand Down Expand Up @@ -175,6 +202,8 @@ def log_sensors(self, file):
data.append(('out_temp', self.outside_temperature))
if self.support_compressor_frequency:
data.append(('cmp_freq', self.compressor_frequency))
if self.support_filter_dirty:
data.append(('en_filter_sign', self.filter_dirty))
if self.support_energy_consumption:
data.append(
('total_today', self.energy_consumption(ATTR_TOTAL, TIME_TODAY))
Expand All @@ -201,6 +230,8 @@ def show_sensors(self):
data.append(f'out_temp={int(self.outside_temperature)}°C')
if self.support_compressor_frequency:
data.append(f'cmp_freq={int(self.compressor_frequency)}Hz')
if self.support_filter_dirty:
data.append(f'en_filter_sign={int(self.filter_dirty)}')
if self.support_energy_consumption:
data.append(
f'total_today={self.energy_consumption(ATTR_TOTAL, TIME_TODAY):.01f}kWh'
Expand Down Expand Up @@ -281,6 +312,20 @@ def support_compressor_frequency(self) -> bool:
"""Return True if the device supports compressor frequency."""
return 'cmpfreq' in self.values

@property
def support_filter_dirty(self) -> bool:
"""Return True if the device supports dirty filter notification and it is turned on."""
return (
'en_filter_sign' in self.values
and 'filter_sign_info' in self.values
and int(self._parse_number('en_filter_sign')) == 1
)

@property
def support_zone_count(self) -> bool:
"""Return True if the device supports count of active zones."""
return 'en_zone' in self.values

@property
def support_energy_consumption(self) -> bool:
"""Return True if the device supports energy consumption monitoring."""
Expand All @@ -306,6 +351,16 @@ def compressor_frequency(self) -> Optional[float]:
"""Return current compressor frequency."""
return self._parse_number('cmpfreq')

@property
def filter_dirty(self) -> Optional[float]:
"""Return current status of the filter."""
return self._parse_number('filter_sign_info')

@property
def zone_count(self) -> Optional[float]:
"""Return number of enabled zones."""
return self._parse_number('en_zone')

@property
def humidity(self) -> Optional[float]:
"""Return current humidity."""
Expand Down
1 change: 1 addition & 0 deletions pydaikin/daikin_brp069.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class DaikinBRP069(Appliance):
'en_hol': 'away_mode',
'cur': 'internal clock',
'adv': 'advanced mode',
'filter_sign_info': 'filter dirty',
}

async def init(self):
Expand Down
2 changes: 1 addition & 1 deletion pydaikin/daikin_brp072c.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
class DaikinBRP072C(DaikinBRP069):
"""Daikin class for BRP072Cxx units."""

def __init__(self, device_id, session=None, key=None, uuid=None):
def __init__(self, device_id, session=None, key=None, uuid=None) -> None:
"""Init the pydaikin appliance, representing one Daikin AirBase
(BRP15B61) device."""
super().__init__(device_id, session)
Expand Down
2 changes: 1 addition & 1 deletion pydaikin/daikin_skyfi.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class DaikinSkyFi(Appliance):
},
}

def __init__(self, device_id, session=None, password=None):
def __init__(self, device_id, session=None, password=None) -> None:
"""Init the pydaikin appliance, representing one Daikin SkyFi device."""
super().__init__(device_id, session)
self.device_ip = f'{self.device_ip}:2000'
Expand Down
4 changes: 3 additions & 1 deletion pydaikin/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class Discovery: # pylint: disable=too-few-public-methods
"""Discovery class."""

def __init__(self):
def __init__(self) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Expand All @@ -37,11 +37,13 @@ def poll(self, stop_if_found=None, ip=None): # pylint: disable=invalid-name
broadcast_ips = [ip]
else:
# get all IPv4 definitions in the system
# pylint: disable=c-extension-no-member
fredrike marked this conversation as resolved.
Show resolved Hide resolved
net_groups = [
netifaces.ifaddresses(i)[netifaces.AF_INET]
for i in netifaces.interfaces()
if netifaces.AF_INET in netifaces.ifaddresses(i)
]
# pylint: enable=c-extension-no-member
fredrike marked this conversation as resolved.
Show resolved Hide resolved

# flatten the previous list
net_ips = [item for sublist in net_groups for item in sublist]
Expand Down
21 changes: 16 additions & 5 deletions pydaikin/factory.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"Factory to generate Pydaikin complete objects"

import logging
from typing import Optional

from aiohttp import ClientSession
from aiohttp.web_exceptions import HTTPNotFound

from .daikin_airbase import DaikinAirBase
from .daikin_base import Appliance
Expand All @@ -11,6 +13,8 @@
from .daikin_skyfi import DaikinSkyFi
from .exceptions import DaikinException

_LOGGER = logging.getLogger(__name__)


class DaikinFactory: # pylint: disable=too-few-public-methods
"Factory object generating instantiated instances of Appliance"
Expand Down Expand Up @@ -42,11 +46,16 @@ async def __init__(
uuid=kwargs.get('uuid'),
)
else: # special case for BRP069 and AirBase
self._generated_object = DaikinBRP069(device_id, session)
await self._generated_object.update_status(
self._generated_object.HTTP_RESOURCES[:1]
)
if not self._generated_object.values:
try:
_LOGGER.debug("Trying connection to BRP069")
self._generated_object = DaikinBRP069(device_id, session)
await self._generated_object.update_status(
self._generated_object.HTTP_RESOURCES[:1]
)
if not self._generated_object.values:
raise DaikinException("Empty Values.")
except (HTTPNotFound, DaikinException) as err:
_LOGGER.debug("Falling back to AirBase: %s", err)
self._generated_object = DaikinAirBase(device_id, session)

await self._generated_object.init()
Expand All @@ -55,3 +64,5 @@ async def __init__(
raise DaikinException(
f"Error creating device, {device_id} is not supported."
)

_LOGGER.debug("Daikin generated object: %s", self._generated_object)
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
netifaces
aiohttp
urllib3
retry
tenacity
kingy444 marked this conversation as resolved.
Show resolved Hide resolved