Skip to content

Commit

Permalink
Merge branch 'fredrike:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
kingy444 authored Jun 3, 2024
2 parents 0f2d5b4 + 4c3f920 commit f3a7bbe
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.11.1
current_version = 2.12.0
commit = True
tag = True
files = setup.py
26 changes: 26 additions & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Pylint

on:
push:
pull_request:

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
pip install -r requirements.txt
- name: Analysing the code with pylint
run: |
pylint --extension-pkg-whitelist=netifaces bin/pydaikin pydaikin/*.py
39 changes: 39 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Upload Python Package

on:
release:
types: [published]

permissions:
contents: read

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
11 changes: 6 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ default_language_version:
python: python3.11
repos:
- repo: https://github.com/psf/black
rev: 24.2.0
rev: 24.4.2
hooks:
- id: black
args:
Expand All @@ -17,19 +17,20 @@ repos:
additional_dependencies:
#- flake8-docstrings==1.5.0
- pydocstyle==5.0.2
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pycqa/pylint
rev: v3.0.3
rev: v3.2.2
hooks:
- id: pylint
additional_dependencies:
- 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
- --extension-pkg-allow-list=netifaces
26 changes: 20 additions & 6 deletions pydaikin/daikin_airbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ def parse_response(response_body):

return response

def __init__(
def __init__( # pylint:disable=useless-parent-delegation
self, device_id, session=None
): # pylint:disable=useless-super-delegation
) -> None:
"""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
2 changes: 1 addition & 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 Down
Loading

0 comments on commit f3a7bbe

Please sign in to comment.