From bed4cb87c1eb7178316afc17d6225c9f853bc4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=20Kampbj=C3=B8rn?= Date: Tue, 9 Jun 2026 12:51:52 +0200 Subject: [PATCH] fix: use HomeAssistant's httpx_client for getting async client And let ruff reformat existing python code --- custom_components/touchline/__init__.py | 22 ++++++- custom_components/touchline/climate.py | 1 + custom_components/touchline/config_flow.py | 10 ++-- custom_components/touchline/manifest.json | 2 +- pyproject.toml | 29 ++++++++++ tests/test_init.py | 67 ++++++++++++++++++++++ 6 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 pyproject.toml create mode 100644 tests/test_init.py diff --git a/custom_components/touchline/__init__.py b/custom_components/touchline/__init__.py index ee45c27..9719acd 100644 --- a/custom_components/touchline/__init__.py +++ b/custom_components/touchline/__init__.py @@ -2,15 +2,20 @@ from __future__ import annotations +from typing import override + from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import httpx_client from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType +from httpx import AsyncClient from pytouchline_extended import PyTouchline + from .const import _LOGGER, DOMAIN PLATFORMS = [Platform.CLIMATE] @@ -75,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host, ) - py_touchline = PyTouchline(url=host) + py_touchline = HassTouchline(hass=hass, url=host) number_of_devices = await py_touchline.get_number_of_devices_async() _LOGGER.debug( @@ -104,3 +109,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class HassTouchline(PyTouchline): + def __init__(self, hass: HomeAssistant, url: str): + super().__init__(url=url) + self.hass = hass + self._async_client = httpx_client.create_async_httpx_client( + hass, + verify_ssl=False, + timeout=self._timeout, + ) + + @override + def get_async_client(self) -> AsyncClient: + return self._async_client diff --git a/custom_components/touchline/climate.py b/custom_components/touchline/climate.py index ad718bc..fbd7e1b 100644 --- a/custom_components/touchline/climate.py +++ b/custom_components/touchline/climate.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from pytouchline_extended import PyTouchline + from .const import _LOGGER, DOMAIN diff --git a/custom_components/touchline/config_flow.py b/custom_components/touchline/config_flow.py index 0ce2b22..cc03c2b 100644 --- a/custom_components/touchline/config_flow.py +++ b/custom_components/touchline/config_flow.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from pytouchline_extended import PyTouchline + from .const import _LOGGER, DOMAIN DATA_SCHEMA = vol.Schema( @@ -25,7 +26,7 @@ async def _async_try_connect_and_fetch_basic_info(host): """Attempt to connect and, if successful, fetch number of devices.""" py_touchline = PyTouchline(url=host) - result = {"type": None, "data": {}} + result = {"type": None, "data": None} number_of_devices = None device = PyTouchline(id=0, url=host) try: @@ -65,7 +66,6 @@ async def async_step_user( result = {"type": None, "data": {}} if user_input is not None: - # Abort if an entry with same host is present. self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) @@ -93,12 +93,14 @@ async def async_step_user( user_input[CONF_HOST], ) if not errors: - return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + return self.async_create_entry( + title=user_input[CONF_HOST], data=user_input + ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, - errors = errors, + errors=errors, ) async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: diff --git a/custom_components/touchline/manifest.json b/custom_components/touchline/manifest.json index dc6b6ba..0577b65 100644 --- a/custom_components/touchline/manifest.json +++ b/custom_components/touchline/manifest.json @@ -1,7 +1,7 @@ { "domain": "touchline", "name": "Roth Touchline", - "version": "0.1.2", + "version": "0.1.3", "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/touchline", diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5344cc1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "hass-touchline" +version = "0.1.3" +description = "Home Assistant custom component using pytouchline_extended lib." +readme = "README.md" +authors = [ + { name = "Peter Brøndum", email = "peter.brondum@bestseller.com" }, + { name = "Hack Kampbjørn", email = "com.github@hack.kampbjorn.com" } +] +dependencies = [ + "pytouchline_extended==1.1.2" +] +requires-python = ">=3.12" + +[project.optional-dependencies] +test = [ + "pytest==9.0.0", + "pytest-homeassistant-custom-component==0.13.315" +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +norecursedirs = [".git", "venv-*"] +asyncio_mode = "auto" +pythonpath = ["."] diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..8e1b2d7 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,67 @@ +"""Tests for Roth Touchline setup.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +# Import from the integration +from custom_components.touchline import async_setup_entry +from custom_components.touchline.const import DOMAIN + + +@pytest.fixture +def mock_config_entry(): + """Mock a config entry.""" + entry = MagicMock() + entry.data = {CONF_HOST: "http://127.0.0.2"} + entry.entry_id = "test_entry_id" + entry.state = ConfigEntryState.NOT_LOADED + return entry + + +@patch("custom_components.touchline.HassTouchline") +async def test_setup_entry_success( + mock_hass_touchline, hass: HomeAssistant, mock_config_entry +): + """Test successful setup of a config entry.""" + mock_instance = mock_hass_touchline.return_value + mock_instance.get_number_of_devices_async = AsyncMock(return_value=5) + + with patch.object( + hass.config_entries, "async_forward_entry_setups", return_value=True + ) as mock_forward: + assert await async_setup_entry(hass, mock_config_entry) is True + + assert DOMAIN in hass.data + assert hass.data[DOMAIN][mock_config_entry.entry_id] == mock_instance + assert mock_forward.called + + +@patch("custom_components.touchline.HassTouchline") +async def test_setup_entry_no_devices( + mock_hass_touchline, hass: HomeAssistant, mock_config_entry +): + """Test setup failure when no devices are found.""" + mock_instance = mock_hass_touchline.return_value + mock_instance.get_number_of_devices_async = AsyncMock(return_value=0) + + with pytest.raises(ConfigEntryNotReady): + await async_setup_entry(hass, mock_config_entry) + + +@patch("custom_components.touchline.HassTouchline") +async def test_setup_entry_exception( + mock_hass_touchline, hass: HomeAssistant, mock_config_entry +): + """Test setup failure when an exception occurs during device check.""" + mock_instance = mock_hass_touchline.return_value + mock_instance.get_number_of_devices_async = AsyncMock( + side_effect=Exception("Connection error") + ) + + with pytest.raises(Exception): + await async_setup_entry(hass, mock_config_entry)