Skip to content

Commit

Permalink
Add Komfovent (home-assistant#95722)
Browse files Browse the repository at this point in the history
* komfovent integration V1

* add dependency

* integrate komfovent api

* fix errors found in testing

* tests for form handling

* update deps

* update coverage rc

* add correct naming

* minor feedback

* pre-commit fixes

* feedback fixes part 1 of 2

* feedback fixes part 2 of 2

* add hvac mode support

* fix tests

* address feedback

* fix code coverage + PR feedback

* PR feedback

* use device name

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
  • Loading branch information
2 people authored and dgomes committed Nov 11, 2023
1 parent 98dcf65 commit 21f7ddc
Show file tree
Hide file tree
Showing 15 changed files with 454 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,8 @@ omit =
homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/media_player.py
homeassistant/components/kodi/notify.py
homeassistant/components/komfovent/__init__.py
homeassistant/components/komfovent/climate.py
homeassistant/components/konnected/__init__.py
homeassistant/components/konnected/panel.py
homeassistant/components/konnected/switch.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,8 @@ build.json @home-assistant/supervisor
/tests/components/knx/ @Julius2342 @farmio @marvin-w
/homeassistant/components/kodi/ @OnFreund
/tests/components/kodi/ @OnFreund
/homeassistant/components/komfovent/ @ProstoSanja
/tests/components/komfovent/ @ProstoSanja
/homeassistant/components/konnected/ @heythisisnate
/tests/components/konnected/ @heythisisnate
/homeassistant/components/kostal_plenticore/ @stegm
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/komfovent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""The Komfovent integration."""
from __future__ import annotations

import komfovent_api

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .const import DOMAIN

PLATFORMS: list[Platform] = [Platform.CLIMATE]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Komfovent from a config entry."""
host = entry.data[CONF_HOST]
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
_, credentials = komfovent_api.get_credentials(host, username, password)
result, settings = await komfovent_api.get_settings(credentials)
if result != komfovent_api.KomfoventConnectionResult.SUCCESS:
raise ConfigEntryNotReady(f"Unable to connect to {host}: {result}")

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (credentials, settings)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
91 changes: 91 additions & 0 deletions homeassistant/components/komfovent/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Ventilation Units from Komfovent integration."""
from __future__ import annotations

import komfovent_api

from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN

HASS_TO_KOMFOVENT_MODES = {
HVACMode.COOL: komfovent_api.KomfoventModes.COOL,
HVACMode.HEAT_COOL: komfovent_api.KomfoventModes.HEAT_COOL,
HVACMode.OFF: komfovent_api.KomfoventModes.OFF,
HVACMode.AUTO: komfovent_api.KomfoventModes.AUTO,
}
KOMFOVENT_TO_HASS_MODES = {v: k for k, v in HASS_TO_KOMFOVENT_MODES.items()}


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Komfovent unit control."""
credentials, settings = hass.data[DOMAIN][entry.entry_id]
async_add_entities([KomfoventDevice(credentials, settings)], True)


class KomfoventDevice(ClimateEntity):
"""Representation of a ventilation unit."""

_attr_hvac_modes = list(HASS_TO_KOMFOVENT_MODES.keys())
_attr_preset_modes = [mode.name for mode in komfovent_api.KomfoventPresets]
_attr_supported_features = ClimateEntityFeature.PRESET_MODE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_has_entity_name = True
_attr_name = None

def __init__(
self,
credentials: komfovent_api.KomfoventCredentials,
settings: komfovent_api.KomfoventSettings,
) -> None:
"""Initialize the ventilation unit."""
self._komfovent_credentials = credentials
self._komfovent_settings = settings

self._attr_unique_id = settings.serial_number
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, settings.serial_number)},
model=settings.model,
name=settings.name,
serial_number=settings.serial_number,
sw_version=settings.version,
manufacturer="Komfovent",
)

async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
await komfovent_api.set_preset(
self._komfovent_credentials,
komfovent_api.KomfoventPresets[preset_mode],
)

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
await komfovent_api.set_mode(
self._komfovent_credentials, HASS_TO_KOMFOVENT_MODES[hvac_mode]
)

async def async_update(self) -> None:
"""Get the latest data."""
result, status = await komfovent_api.get_unit_status(
self._komfovent_credentials
)
if result != komfovent_api.KomfoventConnectionResult.SUCCESS or not status:
self._attr_available = False
return
self._attr_available = True
self._attr_preset_mode = status.preset
self._attr_current_temperature = status.temp_extract
self._attr_hvac_mode = KOMFOVENT_TO_HASS_MODES[status.mode]
74 changes: 74 additions & 0 deletions homeassistant/components/komfovent/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Config flow for Komfovent integration."""
from __future__ import annotations

import logging
from typing import Any

import komfovent_api
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER = "user"
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_USERNAME, default="user"): str,
vol.Required(CONF_PASSWORD): str,
}
)

ERRORS_MAP = {
komfovent_api.KomfoventConnectionResult.NOT_FOUND: "cannot_connect",
komfovent_api.KomfoventConnectionResult.UNAUTHORISED: "invalid_auth",
komfovent_api.KomfoventConnectionResult.INVALID_INPUT: "invalid_input",
}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Komfovent."""

VERSION = 1

def __return_error(
self, result: komfovent_api.KomfoventConnectionResult
) -> FlowResult:
return self.async_show_form(
step_id=STEP_USER,
data_schema=STEP_USER_DATA_SCHEMA,
errors={"base": ERRORS_MAP.get(result, "unknown")},
)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id=STEP_USER, data_schema=STEP_USER_DATA_SCHEMA
)

conf_host = user_input[CONF_HOST]
conf_username = user_input[CONF_USERNAME]
conf_password = user_input[CONF_PASSWORD]

result, credentials = komfovent_api.get_credentials(
conf_host, conf_username, conf_password
)
if result != komfovent_api.KomfoventConnectionResult.SUCCESS:
return self.__return_error(result)

result, settings = await komfovent_api.get_settings(credentials)
if result != komfovent_api.KomfoventConnectionResult.SUCCESS:
return self.__return_error(result)

await self.async_set_unique_id(settings.serial_number)
self._abort_if_unique_id_configured()

return self.async_create_entry(title=settings.name, data=user_input)
3 changes: 3 additions & 0 deletions homeassistant/components/komfovent/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for the Komfovent integration."""

DOMAIN = "komfovent"
9 changes: 9 additions & 0 deletions homeassistant/components/komfovent/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain": "komfovent",
"name": "Komfovent",
"codeowners": ["@ProstoSanja"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/komfovent",
"iot_class": "local_polling",
"requirements": ["komfovent-api==0.0.3"]
}
22 changes: 22 additions & 0 deletions homeassistant/components/komfovent/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_input": "Failed to parse provided hostname",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
"kmtronic",
"knx",
"kodi",
"komfovent",
"konnected",
"kostal_plenticore",
"kraken",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -2881,6 +2881,12 @@
"config_flow": true,
"iot_class": "local_push"
},
"komfovent": {
"name": "Komfovent",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"konnected": {
"name": "Konnected.io",
"integration_type": "hub",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,9 @@ kiwiki-client==0.1.1
# homeassistant.components.knx
knx-frontend==2023.6.23.191712

# homeassistant.components.komfovent
komfovent-api==0.0.3

# homeassistant.components.konnected
konnected==1.2.0

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,9 @@ kegtron-ble==0.4.0
# homeassistant.components.knx
knx-frontend==2023.6.23.191712

# homeassistant.components.komfovent
komfovent-api==0.0.3

# homeassistant.components.konnected
konnected==1.2.0

Expand Down
1 change: 1 addition & 0 deletions tests/components/komfovent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Komfovent integration."""
14 changes: 14 additions & 0 deletions tests/components/komfovent/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Common fixtures for the Komfovent tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch

import pytest


@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.komfovent.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry

0 comments on commit 21f7ddc

Please sign in to comment.