Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ ignored-parents=
max-args=8

# Maximum number of attributes for a class (see R0902).
max-attributes=10
max-attributes=13

# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
Expand Down
160 changes: 137 additions & 23 deletions custom_components/compit/api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import logging
import asyncio
import logging
from typing import Any
from .types.DeviceState import DeviceState
from .types.SystemInfo import SystemInfo
from .const import API_URL

import aiohttp
import async_timeout

from .const import API_URL
from .types.DeviceState import DeviceState
from .types.SystemInfo import SystemInfo

TIMEOUT = 10
_LOGGER: logging.Logger = logging.getLogger(__package__)
HEADERS = {"Content-type": "application/json; charset=UTF-8"}
Expand All @@ -20,6 +22,18 @@ def __init__(self, email, password, session: aiohttp.ClientSession):
self._api_wrapper = ApiWrapper(session)

async def authenticate(self):
"""
Handles user authentication asynchronously by interacting with an API endpoint.

Raises:
Exception: Captures and logs any exception encountered during the
authentication process.

Returns:
SystemInfo | bool: Returns a SystemInfo object created from the successful
response data, or False in case of an error or failure during the
authentication process.
"""
try:
response = await self._api_wrapper.post(
f"{API_URL}/authorize",
Expand Down Expand Up @@ -55,6 +69,22 @@ async def authenticate(self):
return False

async def get_gates(self):
"""
Retrieves the gates information asynchronously.

This method interacts with an external API to fetch the information about
available gates. In case of an error during the fetching process, it logs
the exception and returns False.

Raises:
Exception: General exception raised during API communication or data
transformation.

Returns:
SystemInfo: A SystemInfo object populated with data fetched from the
API if successful.
bool: Returns False when an error occurs.
"""
try:
response = await self._api_wrapper.get(f"{API_URL}/gates", {}, self.token)

Expand All @@ -64,6 +94,27 @@ async def get_gates(self):
return False

async def get_state(self, device_id: int):
"""
Fetches the state of a device using its unique device identifier asynchronously.

This method interacts with an external API to retrieve the current state of
the specified device. It uses an internal API wrapper to perform the HTTP GET
request to the desired endpoint and processes the response to return a
DeviceState object representing the device's state. In case of an error, it
logs the exception and returns False.

Args:
device_id (int): The unique identifier of the device for which the state
is to be fetched.

Returns:
DeviceState | bool: A DeviceState object parsed from the API response if
successful, otherwise False.

Raises:
Any exception occurring during the API call or response processing will
be logged and returned as False without being re-raised.
"""
try:
response = await self._api_wrapper.get(
f"{API_URL}/devices/{device_id}/state", {}, self.token
Expand All @@ -76,12 +127,28 @@ async def get_state(self, device_id: int):
return False

async def update_device_parameter(
self, device_id: int, parameter: str, value: str | int
self, device_id: int, parameter: str, value: str | int
):
try:
print(f"Set {parameter} to {value} for device {device_id}")
_LOGGER.info(f"Set {parameter} to {value} for device {device_id}")
"""
Updates a device parameter by sending a request to the device API.

This method allows updating the configuration parameter of a specific device
with the given value. It logs the request and processes the response from the
API upon completion. If the operation fails, it logs the error and returns
False.

Parameters:
device_id (int): The unique identifier of the device.
parameter (str): The name of the parameter to be updated.
value (str | int): The new value to set for the specified parameter.

Returns:
bool: Returns a boolean indicating the success of the operation. If the
operation is successful, it returns the result of the API response, else
returns False.
"""
try:
_LOGGER.info("Set %s to %s for device %s", parameter, value, device_id)
data = {"values": [{"code": parameter, "value": value}]}

response = await self._api_wrapper.put(
Expand All @@ -94,8 +161,27 @@ async def update_device_parameter(
return False

async def get_result(
self, response: aiohttp.ClientResponse, ignore_response_code: bool = False
self, response: aiohttp.ClientResponse, ignore_response_code: bool = False
) -> Any:
"""
Asynchronously retrieves and processes the JSON response from an aiohttp.ClientResponse
object. Allows for optional ignoring of response status codes.

Parameters:
response: aiohttp.ClientResponse
The HTTP response object received from the aiohttp request.
ignore_response_code: bool, optional
A boolean indicating whether to ignore the response status code. Defaults to False.

Returns:
Any
The JSON-decoded response content.

Raises:
Exception
If the response status code is not successful (non-2xx) and ignore_response_code
is False.
"""
if response.ok or ignore_response_code:
return await response.json()

Expand All @@ -109,18 +195,24 @@ def __init__(self, session: aiohttp.ClientSession):
self._session = session

async def get(
self, url: str, headers: dict = {}, auth: Any = None
self, url: str, headers=None, auth: Any = None
) -> aiohttp.ClientResponse:
"""Run http GET method"""
if headers is None:
headers = {}
if auth:
headers["Authorization"] = auth

return await self.api_wrapper("get", url, headers=headers, auth=None)

async def post(
self, url: str, data: dict = {}, headers: dict = {}, auth: Any = None
self, url: str, data=None, headers=None, auth: Any = None
) -> aiohttp.ClientResponse:
"""Run http POST method"""
if headers is None:
headers = {}
if data is None:
data = {}
if auth:
headers["Authorization"] = auth

Expand All @@ -129,43 +221,65 @@ async def post(
)

async def put(
self, url: str, data: dict = {}, headers: dict = {}, auth: Any = None
self, url: str, data=None, headers=None, auth: Any = None
) -> aiohttp.ClientResponse:
"""Run http PUT method"""
if headers is None:
headers = {}
if data is None:
data = {}
if auth:
headers["Authorization"] = auth

return await self.api_wrapper("put", url, data=data, headers=headers, auth=None)

async def api_wrapper(
self,
method: str,
url: str,
data: dict = {},
headers: dict = {},
auth: Any = None,
self,
method: str,
url: str,
data: dict = None,
headers: dict = None,
auth: Any = None,
) -> Any:
"""Get information from the API."""
# Use None as default and create a new dict if needed
if data is None:
data = {}
if headers is None:
headers = {}

try:
async with async_timeout.timeout(TIMEOUT):
if method == "get":
if method.lower() == "get":
response = await self._session.get(url, headers=headers, auth=auth)
return response

elif method == "post":
elif method.lower() == "post":
response = await self._session.post(
url, headers=headers, data=data, auth=auth
url,
headers=headers,
json=data,
auth=auth, # Use JSON for consistency
)
return response
elif method == "put":
elif method.lower() == "put":
response = await self._session.put(
url, headers=headers, json=data, auth=auth
)
return response
else:
raise ValueError(f"Unsupported HTTP method: {method}")

except asyncio.TimeoutError as exception:
_LOGGER.error(
"Timeout error fetching information from %s - %s",
url,
exception,
)
raise # Re-raise the exception instead of returning None
except Exception as exception:
_LOGGER.error(
"Error fetching information from %s - %s",
url,
exception,
)
raise # Re-raise the exception
41 changes: 36 additions & 5 deletions custom_components/compit/climate.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
import logging
from typing import List
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, MANURFACER_NAME
from .coordinator import CompitDataUpdateCoordinator
from .types.DeviceDefinitions import Parameter
from .types.SystemInfo import Device
from .coordinator import CompitDataUpdateCoordinator
from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from .const import DOMAIN, MANURFACER_NAME

_LOGGER: logging.Logger = logging.getLogger(__package__)


async def async_setup_entry(hass: HomeAssistant, entry, async_add_devices):
"""
Sets up the asynchronous platform entry for CompitClimate devices.

This function initializes and adds CompitClimate devices to the platform by iterating
over the devices provided by the coordinator. It filters and maps the necessary device
data and definitions required for the platform.

Parameters:
hass (HomeAssistant): The HomeAssistant instance.
entry: The configuration entry to set up.
async_add_devices (Callable[[List[CompitClimate]], Awaitable[None]]): Asynchronous
function to add the CompitClimate devices.

Raises:
Exception: Raised if any internal error occurs during the setup.

"""
coordinator: CompitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]

async_add_devices(
Expand Down Expand Up @@ -53,6 +71,9 @@ def __init__(
device_name: str,
):
super().__init__(coordinator)
self._hvac_mode = None
self._fan_mode = None
self._preset_mode = None
self.coordinator = coordinator
self.unique_id = f"{device.label}_climate"
self.label = f"{device.label} climate"
Expand All @@ -69,6 +90,16 @@ def __init__(
self.set_initial_values()

def set_initial_values(self):
"""
Sets the initial values for thermostat mode, fan mode, and HVAC (Heating, Ventilation, and Air Conditioning)
mode based on the current parameters provided by the coordinator. It retrieves each relevant parameter
and determines the appropriate current mode, such as thermostat preset mode, fan operating mode,
and HVAC operation.

Raises:
KeyError: If a required key is missing in the state or its parameters.

"""
preset_mode = self.coordinator.data[self.device.id].state.get_parameter_value(
"__trybpracytermostatu"
)
Expand Down
Loading