From 755d60efeb1891b70c22adb529137a236f6544cd Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 12:55:44 -0400 Subject: [PATCH 01/13] Lint and add workflow --- .github/workflows/hassfest.yml | 18 ++++++++++++++ README.md | 9 +++---- custom_components/leslies_pool/config_flow.py | 3 +-- custom_components/leslies_pool/sensor.py | 3 +-- tests/conftest.py | 1 + tests/const.py | 8 ++----- tests/test_api.py | 6 ++--- tests/test_config_flow.py | 20 +++++++--------- tests/test_init.py | 24 ++++++++----------- tests/test_switch.py | 24 ++++++------------- 10 files changed, 56 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/hassfest.yml diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml new file mode 100644 index 0000000..410922d --- /dev/null +++ b/.github/workflows/hassfest.yml @@ -0,0 +1,18 @@ +name: Run HassFest + +on: + pull_request: + paths: + - "homeassistant/**" + - ".github/workflows/hassfest.yml" + +jobs: + hassfest: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Run HassFest + uses: home-assistant/actions/hassfest@main diff --git a/README.md b/README.md index 2a0acec..c4585e4 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,12 @@ **This component will set up the following platforms.** -| Platform | Description | -| --------------- | ------------------------------------------------------------------------- | -| `sensor` | Show info from leslies_pool API. | +| Platform | Description | +| -------- | -------------------------------- | +| `sensor` | Show info from leslies_pool API. | **The component will set up the following sensors:** + - Free Chlorine - PPM - Total Chlorine - PPM - PH - pH @@ -34,7 +35,6 @@ - Phosphates - PPB - Salt - PPM - ## Installation - Automatic (REQUIRES HACS) 1. Add this repository URL to HACS custom repositories as an Integration @@ -53,6 +53,7 @@ 7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "leslies_pool" ## Setup + 1. Provide the Username and Password for your leslie's account. These are used to auth and refresh cookies 2. Input the Water Test URL. This can be found by navigating [here](https://lesliespool.com/on/demandware.store/Sites-lpm_site-Site/en_US/PoolProfile-Landing) once logged in, and then by clicking on "Water Tests" for the pool you want to integrate. The water test URL can be copied from the URL bar once you have navigated there. This URL contains the Pool ID and Pool Name which are needed to make the API calls to fetch the data. 3. Set a polling rate (Seconds). diff --git a/custom_components/leslies_pool/config_flow.py b/custom_components/leslies_pool/config_flow.py index 1adc090..d9bd034 100644 --- a/custom_components/leslies_pool/config_flow.py +++ b/custom_components/leslies_pool/config_flow.py @@ -6,12 +6,11 @@ import re from typing import Any -import voluptuous as vol - from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +import voluptuous as vol from .api import LesliesPoolApi from .const import DOMAIN diff --git a/custom_components/leslies_pool/sensor.py b/custom_components/leslies_pool/sensor.py index f862c21..e605f62 100644 --- a/custom_components/leslies_pool/sensor.py +++ b/custom_components/leslies_pool/sensor.py @@ -3,14 +3,13 @@ from datetime import timedelta import logging -import requests - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import requests from .const import DOMAIN diff --git a/tests/conftest.py b/tests/conftest.py index 857df35..21706d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Global fixtures for leslies_pool integration.""" + from unittest.mock import patch import pytest diff --git a/tests/const.py b/tests/const.py index 80c8916..92cfc18 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,9 +1,5 @@ """Constants for leslies_pool tests.""" -from custom_components.leslies_pool.const import ( - CONF_PASSWORD, -) -from custom_components.leslies_pool.const import ( - CONF_USERNAME, -) + +from custom_components.leslies_pool.const import CONF_PASSWORD, CONF_USERNAME MOCK_CONFIG = {CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password"} diff --git a/tests/test_api.py b/tests/test_api.py index 16c4b97..4e3d142 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,12 +1,12 @@ """Tests for leslies_pool api.""" + import asyncio import aiohttp -from custom_components.leslies_pool.api import ( - LesliesPoolApiClient, -) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from custom_components.leslies_pool.api import LesliesPoolApiClient + async def test_api(hass, aioclient_mock, caplog): """Test API calls.""" diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 62792ce..fdde29d 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,25 +1,18 @@ """Test leslies_pool config flow.""" + from unittest.mock import patch +from homeassistant import config_entries, data_entry_flow import pytest +from pytest_homeassistant_custom_component.common import MockConfigEntry + from custom_components.leslies_pool.const import ( BINARY_SENSOR, -) -from custom_components.leslies_pool.const import ( DOMAIN, -) -from custom_components.leslies_pool.const import ( PLATFORMS, -) -from custom_components.leslies_pool.const import ( SENSOR, -) -from custom_components.leslies_pool.const import ( SWITCH, ) -from homeassistant import config_entries -from homeassistant import data_entry_flow -from pytest_homeassistant_custom_component.common import MockConfigEntry from .const import MOCK_CONFIG @@ -30,7 +23,10 @@ @pytest.fixture(autouse=True) def bypass_setup_fixture(): """Prevent setup.""" - with patch("custom_components.leslies_pool.async_setup", return_value=True,), patch( + with patch( + "custom_components.leslies_pool.async_setup", + return_value=True, + ), patch( "custom_components.leslies_pool.async_setup_entry", return_value=True, ): diff --git a/tests/test_init.py b/tests/test_init.py index d8b436a..7ca7a2d 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,22 +1,16 @@ """Test leslies_pool setup process.""" + +from homeassistant.exceptions import ConfigEntryNotReady import pytest +from pytest_homeassistant_custom_component.common import MockConfigEntry + from custom_components.leslies_pool import ( + LesliesPoolDataUpdateCoordinator, async_reload_entry, -) -from custom_components.leslies_pool import ( async_setup_entry, -) -from custom_components.leslies_pool import ( async_unload_entry, ) -from custom_components.leslies_pool import ( - LesliesPoolDataUpdateCoordinator, -) -from custom_components.leslies_pool.const import ( - DOMAIN, -) -from homeassistant.exceptions import ConfigEntryNotReady -from pytest_homeassistant_custom_component.common import MockConfigEntry +from custom_components.leslies_pool.const import DOMAIN from .const import MOCK_CONFIG @@ -37,14 +31,16 @@ async def test_setup_unload_and_reload_entry(hass, bypass_get_data): assert await async_setup_entry(hass, config_entry) assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] assert ( - type(hass.data[DOMAIN][config_entry.entry_id]) == LesliesPoolDataUpdateCoordinator + type(hass.data[DOMAIN][config_entry.entry_id]) + == LesliesPoolDataUpdateCoordinator ) # Reload the entry and assert that the data from above is still there assert await async_reload_entry(hass, config_entry) is None assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] assert ( - type(hass.data[DOMAIN][config_entry.entry_id]) == LesliesPoolDataUpdateCoordinator + type(hass.data[DOMAIN][config_entry.entry_id]) + == LesliesPoolDataUpdateCoordinator ) # Unload the entry and verify that the data has been removed diff --git a/tests/test_switch.py b/tests/test_switch.py index 1fa4047..a9d0b3c 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1,24 +1,14 @@ """Test leslies_pool switch.""" -from unittest.mock import call -from unittest.mock import patch - -from custom_components.leslies_pool import ( - async_setup_entry, -) -from custom_components.leslies_pool.const import ( - DEFAULT_NAME, -) -from custom_components.leslies_pool.const import ( - DOMAIN, -) -from custom_components.leslies_pool.const import ( - SWITCH, -) -from homeassistant.components.switch import SERVICE_TURN_OFF -from homeassistant.components.switch import SERVICE_TURN_ON + +from unittest.mock import call, patch + +from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID from pytest_homeassistant_custom_component.common import MockConfigEntry +from custom_components.leslies_pool import async_setup_entry +from custom_components.leslies_pool.const import DEFAULT_NAME, DOMAIN, SWITCH + from .const import MOCK_CONFIG From 618b6b2b59ed3cf915416c2cd2c6f8c05288f712 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 12:57:08 -0400 Subject: [PATCH 02/13] update workflow --- .github/workflows/hassfest.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml index 410922d..0a1bd9c 100644 --- a/.github/workflows/hassfest.yml +++ b/.github/workflows/hassfest.yml @@ -1,18 +1,14 @@ -name: Run HassFest +name: Validate with hassfest on: + push: pull_request: - paths: - - "homeassistant/**" - - ".github/workflows/hassfest.yml" + schedule: + - cron: '0 0 * * *' jobs: - hassfest: - runs-on: ubuntu-latest - + validate: + runs-on: "ubuntu-latest" steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Run HassFest - uses: home-assistant/actions/hassfest@main + - uses: "actions/checkout@v4" + - uses: "home-assistant/actions/hassfest@master" \ No newline at end of file From e64c1c6c7b94c54319c95a3307a363b84a5812f2 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:10:13 -0400 Subject: [PATCH 03/13] Lint and add tests --- .../leslies_pool/translations/en.json | 46 +-- tests/__init__.py | 2 +- tests/conftest.py | 43 +-- tests/test_api.py | 159 +++++----- tests/test_config_flow.py | 280 ++++++++++++------ tests/test_init.py | 59 ---- tests/test_sensor.py | 126 ++++++++ tests/test_switch.py | 45 --- 8 files changed, 432 insertions(+), 328 deletions(-) delete mode 100644 tests/test_init.py create mode 100644 tests/test_sensor.py delete mode 100644 tests/test_switch.py diff --git a/custom_components/leslies_pool/translations/en.json b/custom_components/leslies_pool/translations/en.json index df15710..46d0c2e 100644 --- a/custom_components/leslies_pool/translations/en.json +++ b/custom_components/leslies_pool/translations/en.json @@ -1,27 +1,27 @@ { "config": { - "error": { - "invalid_auth": "Authentication failed.", - "invalid_url": "The provided URL is invalid.", - "unknown": "An unknown error occurred." - }, - "step": { - "user": { - "data": { - "password": "Password", - "scan_interval": "Polling Interval (seconds)", - "username": "Username", - "water_test_url": "Water Test URL" - }, - "description": "Set up your Leslie's Pool integration.", - "description_placeholders": { - "password": "Enter your password", - "scan_interval": "300", - "username": "Enter your username", - "water_test_url": "https://lesliespool.com/on/demandware.store/Sites-lpm_site-Site/en_US/WaterTest-Landing?poolProfileId=XXXX&poolName=XXXX" - }, - "title": "Leslie's Pool Water Tests" - } + "error": { + "invalid_auth": "Authentication failed.", + "invalid_url": "The provided URL is invalid.", + "unknown": "An unknown error occurred." + }, + "step": { + "user": { + "data": { + "password": "Password", + "scan_interval": "Polling Interval (seconds)", + "username": "Username", + "water_test_url": "Water Test URL" + }, + "description": "Set up your Leslie's Pool integration.", + "description_placeholders": { + "password": "Enter your password", + "scan_interval": "300", + "username": "Enter your username", + "water_test_url": "https://lesliespool.com/on/demandware.store/Sites-lpm_site-Site/en_US/WaterTest-Landing?poolProfileId=XXXX&poolName=XXXX" + }, + "title": "Leslie's Pool Water Tests" } + } } -} \ No newline at end of file +} diff --git a/tests/__init__.py b/tests/__init__.py index 61467c6..2e88a04 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Tests for leslies_pool integration.""" +"""Tests for the Leslie's Pool Water Tests integration.""" diff --git a/tests/conftest.py b/tests/conftest.py index 21706d4..d4a5a4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,40 +1,15 @@ -"""Global fixtures for leslies_pool integration.""" +"""Common fixtures for the Leslie's Pool Water Tests tests.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch import pytest -pytest_plugins = "pytest_homeassistant_custom_component" - -# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent -# notifications. These calls would fail without this fixture since the persistent_notification -# integration is never loaded during a test. -@pytest.fixture(name="skip_notifications", autouse=True) -def skip_notifications_fixture(): - """Skip notification calls.""" - with patch("homeassistant.components.persistent_notification.async_create"), patch( - "homeassistant.components.persistent_notification.async_dismiss" - ): - yield - - -# This fixture, when used, will result in calls to async_get_data to return None. To have the call -# return a value, we would add the `return_value=` parameter to the patch call. -@pytest.fixture(name="bypass_get_data") -def bypass_get_data_fixture(): - """Skip calls to get data from API.""" - with patch("custom_components.leslies_pool.LesliesPoolApiClient.async_get_data"): - yield - - -# In this fixture, we are forcing calls to async_get_data to raise an Exception. This is useful -# for exception handling. -@pytest.fixture(name="error_on_get_data") -def error_get_data_fixture(): - """Simulate error when retrieving data from API.""" +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" with patch( - "custom_components.leslies_pool.LesliesPoolApiClient.async_get_data", - side_effect=Exception, - ): - yield + "homeassistant.components.leslies_pool.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/test_api.py b/tests/test_api.py index 4e3d142..c8dec6c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,87 +1,96 @@ -"""Tests for leslies_pool api.""" +"""Test the API for Leslie's Pool Water Tests.""" -import asyncio +import unittest +from unittest.mock import MagicMock, patch -import aiohttp -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.components.leslies_pool.api import LesliesPoolApi -from custom_components.leslies_pool.api import LesliesPoolApiClient +class TestLesliesPoolApi(unittest.TestCase): + """Test the Leslie's Pool API.""" -async def test_api(hass, aioclient_mock, caplog): - """Test API calls.""" + def setUp(self): + """Set up the test.""" + self.api = LesliesPoolApi("testuser", "testpassword", "123456", "TestPool") - # To test the api submodule, we first create an instance of our API client - api = LesliesPoolApiClient("test", "test", async_get_clientsession(hass)) + @patch("homeassistant.components.leslies_pool.api.requests.Session.get") + @patch("homeassistant.components.leslies_pool.api.requests.Session.post") + def test_authenticate_success(self, mock_post, mock_get): + """Test successful authentication.""" + login_page_html = '' + mock_get.return_value = MagicMock(status_code=200, text=login_page_html) + mock_post.return_value = MagicMock(status_code=200) - # Use aioclient_mock which is provided by `pytest_homeassistant_custom_components` - # to mock responses to aiohttp requests. In this case we are telling the mock to - # return {"test": "test"} when a `GET` call is made to the specified URL. We then - # call `async_get_data` which will make that `GET` request. - aioclient_mock.get( - "https://jsonplaceholder.typicode.com/posts/1", json={"test": "test"} - ) - assert await api.async_get_data() == {"test": "test"} + result = self.api.authenticate() - # We do the same for `async_set_title`. Note the difference in the mock call - # between the previous step and this one. We use `patch` here instead of `get` - # because we know that `async_set_title` calls `api_wrapper` with `patch` as the - # first parameter - aioclient_mock.patch("https://jsonplaceholder.typicode.com/posts/1") - assert await api.async_set_title("test") is None + assert result + mock_get.assert_called_once_with(self.api.LOGIN_PAGE_URL) + mock_post.assert_called_once_with( + self.api.LOGIN_URL, + headers={ + "accept": "application/json, text/javascript, */*; q=0.01", + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "user-agent": "Mozilla/5.0", + }, + data={ + "loginEmail": "testuser", + "loginPassword": "testpassword", + "csrf_token": "test_csrf_token", + }, + ) - # In order to get 100% coverage, we need to test `api_wrapper` to test the code - # that isn't already called by `async_get_data` and `async_set_title`. Because the - # only logic that lives inside `api_wrapper` that is not being handled by a third - # party library (aiohttp) is the exception handling, we also want to simulate - # raising the exceptions to ensure that the function handles them as expected. - # The caplog fixture allows access to log messages in tests. This is particularly - # useful during exception handling testing since often the only action as part of - # exception handling is a logging statement - caplog.clear() - aioclient_mock.put( - "https://jsonplaceholder.typicode.com/posts/1", exc=asyncio.TimeoutError - ) - assert ( - await api.api_wrapper("put", "https://jsonplaceholder.typicode.com/posts/1") - is None - ) - assert ( - len(caplog.record_tuples) == 1 - and "Timeout error fetching information from" in caplog.record_tuples[0][2] - ) + @patch("homeassistant.components.leslies_pool.api.requests.Session.get") + @patch("homeassistant.components.leslies_pool.api.requests.Session.post") + def test_authenticate_fail(self, mock_post, mock_get): + """Test failed authentication.""" + login_page_html = '' + mock_get.return_value = MagicMock(status_code=200, text=login_page_html) + mock_post.return_value = MagicMock(status_code=401) - caplog.clear() - aioclient_mock.post( - "https://jsonplaceholder.typicode.com/posts/1", exc=aiohttp.ClientError - ) - assert ( - await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/1") - is None - ) - assert ( - len(caplog.record_tuples) == 1 - and "Error fetching information from" in caplog.record_tuples[0][2] - ) + result = self.api.authenticate() - caplog.clear() - aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/2", exc=Exception) - assert ( - await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/2") - is None - ) - assert ( - len(caplog.record_tuples) == 1 - and "Something really wrong happened!" in caplog.record_tuples[0][2] - ) + assert not result - caplog.clear() - aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/3", exc=TypeError) - assert ( - await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/3") - is None - ) - assert ( - len(caplog.record_tuples) == 1 - and "Error parsing information from" in caplog.record_tuples[0][2] - ) + @patch("homeassistant.components.leslies_pool.api.requests.Session.get") + @patch("homeassistant.components.leslies_pool.api.requests.Session.post") + def test_fetch_water_test_data(self, mock_post, mock_get): + """Test fetching water test data.""" + # Mock the response for the landing page request + mock_get.return_value = MagicMock(status_code=200) + + # Mock the response for the water test data request + water_test_html = """ + + + + + + + + + + + + + + + + +
Test1.02.07.080200300.10.23004000
+ """ + mock_post.return_value = MagicMock( + status_code=200, json=MagicMock(return_value={"response": water_test_html}) + ) + + data = self.api.fetch_water_test_data() + + assert data["free_chlorine"] == "1.0" + assert data["total_chlorine"] == "2.0" + assert data["ph"] == "7.0" + assert data["alkalinity"] == "80" + assert data["calcium"] == "200" + assert data["cyanuric_acid"] == "30" + assert data["iron"] == "0.1" + assert data["copper"] == "0.2" + assert data["phosphates"] == "300" + assert data["salt"] == "4000" diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index fdde29d..96dede7 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,113 +1,211 @@ -"""Test leslies_pool config flow.""" +"""Test the Leslie's Pool Water Tests config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -from homeassistant import config_entries, data_entry_flow -import pytest -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.leslies_pool.const import ( - BINARY_SENSOR, - DOMAIN, - PLATFORMS, - SENSOR, - SWITCH, +from homeassistant import config_entries +from homeassistant.components.leslies_pool.config_flow import ( + CannotConnect, + InvalidAuth, + InvalidURL, ) +from homeassistant.components.leslies_pool.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +WATER_TEST_URL = "https://lesliespool.com/on/demandware.store/Sites-lpm_site-Site/en_US/WaterTest-Landing?poolProfileId=5891278&poolName=Pool" -from .const import MOCK_CONFIG +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} -# This fixture bypasses the actual setup of the integration -# since we only want to test the config flow. We test the -# actual functionality of the integration in other test modules. -@pytest.fixture(autouse=True) -def bypass_setup_fixture(): - """Prevent setup.""" with patch( - "custom_components.leslies_pool.async_setup", - return_value=True, - ), patch( - "custom_components.leslies_pool.async_setup_entry", + "homeassistant.components.leslies_pool.api.LesliesPoolApi.authenticate", return_value=True, ): - yield - - -# Here we simiulate a successful config flow from the backend. -# Note that we use the `bypass_get_data` fixture here because -# we want the config flow validation to succeed during the test. -async def test_successful_config_flow(hass, bypass_get_data): - """Test a successful config flow.""" - # Initialize a config flow + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "water_test_url": WATER_TEST_URL, + CONF_SCAN_INTERVAL: 300, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Leslie's Pool" + assert result["data"] == { + "title": "Leslie's Pool", + "username": "test-username", + "password": "test-password", + "pool_profile_id": "5891278", + "pool_name": "Pool", + "scan_interval": 300, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - # Check that the config flow shows the user form as the first step - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - # If a user were to enter `test_username` for username and `test_password` - # for password, it would result in this function call - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_CONFIG - ) - - # Check that the config flow is complete and a new entry is created with - # the input data - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test_username" - assert result["data"] == MOCK_CONFIG - assert result["result"] - - -# In this case, we want to simulate a failure during the config flow. -# We use the `error_on_get_data` mock instead of `bypass_get_data` -# (note the function parameters) to raise an Exception during -# validation of the input config. -async def test_failed_config_flow(hass, error_on_get_data): - """Test a failed config flow due to credential validation failure.""" + with patch( + "homeassistant.components.leslies_pool.api.LesliesPoolApi.authenticate", + side_effect=InvalidAuth, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "water_test_url": WATER_TEST_URL, + CONF_SCAN_INTERVAL: 300, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + with patch( + "homeassistant.components.leslies_pool.api.LesliesPoolApi.authenticate", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "water_test_url": WATER_TEST_URL, + CONF_SCAN_INTERVAL: 300, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Leslie's Pool" + assert result["data"] == { + "title": "Leslie's Pool", + "username": "test-username", + "password": "test-password", + "pool_profile_id": "5891278", + "pool_name": "Pool", + "scan_interval": 300, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_CONFIG - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "auth"} - - -# Our config flow also has an options flow, so we must test it as well. -async def test_options_flow(hass): - """Test an options flow.""" - # Create a new MockConfigEntry and add to HASS (we're bypassing config - # flow entirely) - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - entry.add_to_hass(hass) - - # Initialize an options flow - await hass.config_entries.async_setup(entry.entry_id) - result = await hass.config_entries.options.async_init(entry.entry_id) - - # Verify that the first options step is a user form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + with patch( + "homeassistant.components.leslies_pool.api.LesliesPoolApi.authenticate", + side_effect=CannotConnect, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "water_test_url": WATER_TEST_URL, + CONF_SCAN_INTERVAL: 300, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} - # Enter some fake data into the form - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={platform: platform != SENSOR for platform in PLATFORMS}, + with patch( + "homeassistant.components.leslies_pool.api.LesliesPoolApi.authenticate", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "water_test_url": WATER_TEST_URL, + CONF_SCAN_INTERVAL: 300, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Leslie's Pool" + assert result["data"] == { + "title": "Leslie's Pool", + "username": "test-username", + "password": "test-password", + "pool_profile_id": "5891278", + "pool_name": "Pool", + "scan_interval": 300, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_url( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle invalid URL error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - # Verify that the flow finishes - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test_username" + with patch( + "homeassistant.components.leslies_pool.api.LesliesPoolApi.authenticate", + side_effect=InvalidURL, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "water_test_url": "invalid-url", + CONF_SCAN_INTERVAL: 300, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_url"} - # Verify that the options were updated - assert entry.options == {BINARY_SENSOR: True, SENSOR: False, SWITCH: True} + with patch( + "homeassistant.components.leslies_pool.api.LesliesPoolApi.authenticate", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "water_test_url": WATER_TEST_URL, + CONF_SCAN_INTERVAL: 300, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Leslie's Pool" + assert result["data"] == { + "title": "Leslie's Pool", + "username": "test-username", + "password": "test-password", + "pool_profile_id": "5891278", + "pool_name": "Pool", + "scan_interval": 300, + } + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/test_init.py b/tests/test_init.py deleted file mode 100644 index 7ca7a2d..0000000 --- a/tests/test_init.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Test leslies_pool setup process.""" - -from homeassistant.exceptions import ConfigEntryNotReady -import pytest -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.leslies_pool import ( - LesliesPoolDataUpdateCoordinator, - async_reload_entry, - async_setup_entry, - async_unload_entry, -) -from custom_components.leslies_pool.const import DOMAIN - -from .const import MOCK_CONFIG - - -# We can pass fixtures as defined in conftest.py to tell pytest to use the fixture -# for a given test. We can also leverage fixtures and mocks that are available in -# Home Assistant using the pytest_homeassistant_custom_component plugin. -# Assertions allow you to verify that the return value of whatever is on the left -# side of the assertion matches with the right side. -async def test_setup_unload_and_reload_entry(hass, bypass_get_data): - """Test entry setup and unload.""" - # Create a mock entry so we don't have to go through config flow - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - - # Set up the entry and assert that the values set during setup are where we expect - # them to be. Because we have patched the LesliesPoolDataUpdateCoordinator.async_get_data - # call, no code from custom_components/leslies_pool/api.py actually runs. - assert await async_setup_entry(hass, config_entry) - assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] - assert ( - type(hass.data[DOMAIN][config_entry.entry_id]) - == LesliesPoolDataUpdateCoordinator - ) - - # Reload the entry and assert that the data from above is still there - assert await async_reload_entry(hass, config_entry) is None - assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN] - assert ( - type(hass.data[DOMAIN][config_entry.entry_id]) - == LesliesPoolDataUpdateCoordinator - ) - - # Unload the entry and verify that the data has been removed - assert await async_unload_entry(hass, config_entry) - assert config_entry.entry_id not in hass.data[DOMAIN] - - -async def test_setup_entry_exception(hass, error_on_get_data): - """Test ConfigEntryNotReady when API raises an exception during entry setup.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - - # In this case we are testing the condition where async_setup_entry raises - # ConfigEntryNotReady using the `error_on_get_data` fixture which simulates - # an error. - with pytest.raises(ConfigEntryNotReady): - assert await async_setup_entry(hass, config_entry) diff --git a/tests/test_sensor.py b/tests/test_sensor.py new file mode 100644 index 0000000..93e13f7 --- /dev/null +++ b/tests/test_sensor.py @@ -0,0 +1,126 @@ +"""Test the Leslie's Pool Water Tests sensors.""" + +from datetime import timedelta +import logging +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.leslies_pool.const import DOMAIN +from homeassistant.components.leslies_pool.sensor import ( + LesliesPoolSensor, + async_setup_entry, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + "free_chlorine": ("Free Chlorine", "ppm"), + "total_chlorine": ("Total Chlorine", "ppm"), + "ph": ("pH", "pH"), + "alkalinity": ("Total Alkalinity", "ppm"), + "calcium": ("Calcium Hardness", "ppm"), + "cyanuric_acid": ("Cyanuric Acid", "ppm"), + "iron": ("Iron", "ppm"), + "copper": ("Copper", "ppm"), + "phosphates": ("Phosphates", "ppb"), + "salt": ("Salt", "ppm"), +} + + +@pytest.fixture +def mock_coordinator(hass): + """Mock a coordinator.""" + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="leslies_pool", + update_method=AsyncMock(return_value={sensor: 1 for sensor in SENSOR_TYPES}), + update_interval=timedelta(seconds=300), + ) + coordinator.data = {sensor: 1 for sensor in SENSOR_TYPES} + coordinator.async_refresh = AsyncMock() + coordinator.async_add_listener = AsyncMock() + return coordinator + + +async def test_async_setup_entry(hass, mock_coordinator): + """Test setting up the config entry.""" + mock_entry = AsyncMock() + mock_entry.entry_id = "test_entry" + mock_entry.data = { + "username": "test-username", + "password": "test-password", + "scan_interval": 300, + } + + hass.data = {DOMAIN: {mock_entry.entry_id: mock_coordinator}} + + async_add_entities = AsyncMock() + + with patch( + "homeassistant.components.leslies_pool.api.LesliesPoolApi", + return_value=AsyncMock(), + ): + await async_setup_entry(hass, mock_entry, async_add_entities) + + assert async_add_entities.call_count == 1 + assert len(async_add_entities.call_args[0][0]) == len(SENSOR_TYPES) + + +async def test_sensor_properties(hass, mock_coordinator): + """Test sensor properties.""" + mock_entry = AsyncMock() + mock_entry.entry_id = "test_entry" + + sensor = LesliesPoolSensor( + mock_coordinator, mock_entry, "free_chlorine", "Free Chlorine", "ppm" + ) + + assert sensor.unique_id == "test_entry_free_chlorine" + assert sensor.name == "Free Chlorine" + assert sensor.state == 1 + assert sensor.available + assert sensor.device_info == { + "identifiers": {(DOMAIN, "test_entry")}, + "name": "Leslie's Pool", + "manufacturer": "Leslie's Pool", + "model": "Water Test", + "entry_type": "service", + } + assert sensor.unit_of_measurement == "ppm" + + +async def test_sensor_update(hass, mock_coordinator): + """Test sensor update.""" + mock_entry = AsyncMock() + mock_entry.entry_id = "test_entry" + + sensor = LesliesPoolSensor( + mock_coordinator, mock_entry, "free_chlorine", "Free Chlorine", "ppm" + ) + + with patch.object( + sensor.coordinator, "async_request_refresh", AsyncMock() + ) as mock_refresh: + await sensor.async_update() + assert mock_refresh.call_count == 1 + + +async def test_sensor_added_to_hass(hass, mock_coordinator): + """Test sensor added to hass.""" + mock_entry = AsyncMock() + mock_entry.entry_id = "test_entry" + + sensor = LesliesPoolSensor( + mock_coordinator, mock_entry, "free_chlorine", "Free Chlorine", "ppm" + ) + + with patch.object(sensor, "async_write_ha_state", AsyncMock()): + await sensor.async_added_to_hass() + assert mock_coordinator.async_add_listener.call_count == 1 + assert ( + mock_coordinator.async_add_listener.call_args[0][0] + == sensor.async_write_ha_state + ) diff --git a/tests/test_switch.py b/tests/test_switch.py deleted file mode 100644 index a9d0b3c..0000000 --- a/tests/test_switch.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Test leslies_pool switch.""" - -from unittest.mock import call, patch - -from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.const import ATTR_ENTITY_ID -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from custom_components.leslies_pool import async_setup_entry -from custom_components.leslies_pool.const import DEFAULT_NAME, DOMAIN, SWITCH - -from .const import MOCK_CONFIG - - -async def test_switch_services(hass): - """Test switch services.""" - # Create a mock entry so we don't have to go through config flow - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - assert await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - # Functions/objects can be patched directly in test code as well and can be used to test - # additional things, like whether a function was called or what arguments it was called with - with patch( - "custom_components.leslies_pool.LesliesPoolApiClient.async_set_title" - ) as title_func: - await hass.services.async_call( - SWITCH, - SERVICE_TURN_OFF, - service_data={ATTR_ENTITY_ID: f"{SWITCH}.{DEFAULT_NAME}_{SWITCH}"}, - blocking=True, - ) - assert title_func.called - assert title_func.call_args == call("foo") - - title_func.reset_mock() - - await hass.services.async_call( - SWITCH, - SERVICE_TURN_ON, - service_data={ATTR_ENTITY_ID: f"{SWITCH}.{DEFAULT_NAME}_{SWITCH}"}, - blocking=True, - ) - assert title_func.called - assert title_func.call_args == call("bar") From f4092b35e84c9049a9cb6e772fc626719ad44a10 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:24:35 -0400 Subject: [PATCH 04/13] additional fixes --- custom_components/leslies_pool/manifest.json | 3 ++- custom_components/leslies_pool/translations/en.json | 6 ------ custom_components/leslies_pool/translations/fr.json | 7 ------- custom_components/leslies_pool/translations/nb.json | 7 ------- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/custom_components/leslies_pool/manifest.json b/custom_components/leslies_pool/manifest.json index 5e40fcc..0f23a9d 100644 --- a/custom_components/leslies_pool/manifest.json +++ b/custom_components/leslies_pool/manifest.json @@ -7,5 +7,6 @@ "config_flow": true, "codeowners": ["@connorgallopo"], "requirements": ["beautifulsoup4==4.12.3", "requests==2.31.0"], - "version": "1.0.0" + "version": "1.0.0", + "iot_class": "Cloud Polling" } diff --git a/custom_components/leslies_pool/translations/en.json b/custom_components/leslies_pool/translations/en.json index 46d0c2e..b908aaa 100644 --- a/custom_components/leslies_pool/translations/en.json +++ b/custom_components/leslies_pool/translations/en.json @@ -14,12 +14,6 @@ "water_test_url": "Water Test URL" }, "description": "Set up your Leslie's Pool integration.", - "description_placeholders": { - "password": "Enter your password", - "scan_interval": "300", - "username": "Enter your username", - "water_test_url": "https://lesliespool.com/on/demandware.store/Sites-lpm_site-Site/en_US/WaterTest-Landing?poolProfileId=XXXX&poolName=XXXX" - }, "title": "Leslie's Pool Water Tests" } } diff --git a/custom_components/leslies_pool/translations/fr.json b/custom_components/leslies_pool/translations/fr.json index 795999a..e99b1b7 100644 --- a/custom_components/leslies_pool/translations/fr.json +++ b/custom_components/leslies_pool/translations/fr.json @@ -13,13 +13,6 @@ "username": "Nom d'utilisateur", "water_test_url": "URL du test de l'eau" }, - "description": "Configurez votre intégration Leslie's Pool.", - "description_placeholders": { - "password": "Entrez votre mot de passe", - "scan_interval": "300", - "username": "Entrez votre nom d'utilisateur", - "water_test_url": "https://lesliespool.com/on/demandware.store/Sites-lpm_site-Site/en_US/WaterTest-Landing?poolProfileId=XXXX&poolName=XXXX" - }, "title": "Tests de l'eau de Leslie's Pool" } } diff --git a/custom_components/leslies_pool/translations/nb.json b/custom_components/leslies_pool/translations/nb.json index 9568fae..841337f 100644 --- a/custom_components/leslies_pool/translations/nb.json +++ b/custom_components/leslies_pool/translations/nb.json @@ -13,13 +13,6 @@ "username": "Brukernavn", "water_test_url": "Vanntest-URL" }, - "description": "Konfigurer din Leslie's Pool-integrasjon.", - "description_placeholders": { - "password": "Skriv inn ditt passord", - "scan_interval": "300", - "username": "Skriv inn ditt brukernavn", - "water_test_url": "https://lesliespool.com/on/demandware.store/Sites-lpm_site-Site/en_US/WaterTest-Landing?poolProfileId=XXXX&poolName=XXXX" - }, "title": "Leslie's Pool Vanntester" } } From dae54d488af26a32710ad54af0640c8586d1912a Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:26:46 -0400 Subject: [PATCH 05/13] update manifest --- custom_components/leslies_pool/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/leslies_pool/manifest.json b/custom_components/leslies_pool/manifest.json index 0f23a9d..77e58ae 100644 --- a/custom_components/leslies_pool/manifest.json +++ b/custom_components/leslies_pool/manifest.json @@ -8,5 +8,5 @@ "codeowners": ["@connorgallopo"], "requirements": ["beautifulsoup4==4.12.3", "requests==2.31.0"], "version": "1.0.0", - "iot_class": "Cloud Polling" + "iot_class": "cloud_polling" } From e6df81cd2bbfc7be0b2ddd0604805192c20fe940 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:30:28 -0400 Subject: [PATCH 06/13] more linter fixes --- .github/workflows/hassfest.yml | 6 +++--- custom_components/leslies_pool/__init__.py | 1 - custom_components/leslies_pool/api.py | 4 ++-- custom_components/leslies_pool/config_flow.py | 7 ++++--- custom_components/leslies_pool/requirements.txt | 2 +- custom_components/leslies_pool/sensor.py | 11 ++++++----- requirements.txt | 2 +- tests/conftest.py | 4 ++-- tests/const.py | 4 ++-- tests/test_api.py | 4 ++-- tests/test_config_flow.py | 16 ++++++++-------- tests/test_sensor.py | 13 +++++-------- 12 files changed, 36 insertions(+), 38 deletions(-) diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml index 0a1bd9c..574d2c4 100644 --- a/.github/workflows/hassfest.yml +++ b/.github/workflows/hassfest.yml @@ -4,11 +4,11 @@ on: push: pull_request: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: validate: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v4" - - uses: "home-assistant/actions/hassfest@master" \ No newline at end of file + - uses: "actions/checkout@v4" + - uses: "home-assistant/actions/hassfest@master" diff --git a/custom_components/leslies_pool/__init__.py b/custom_components/leslies_pool/__init__.py index 310c08d..d076386 100644 --- a/custom_components/leslies_pool/__init__.py +++ b/custom_components/leslies_pool/__init__.py @@ -1,5 +1,4 @@ """Initialize Leslie's Pool Water Tests integration.""" - from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/custom_components/leslies_pool/api.py b/custom_components/leslies_pool/api.py index 247dbe7..49acc9d 100644 --- a/custom_components/leslies_pool/api.py +++ b/custom_components/leslies_pool/api.py @@ -1,7 +1,7 @@ """API client for Leslie's Pool Water Tests.""" - -from bs4 import BeautifulSoup, Tag import requests +from bs4 import BeautifulSoup +from bs4 import Tag class LesliesPoolApi: diff --git a/custom_components/leslies_pool/config_flow.py b/custom_components/leslies_pool/config_flow.py index d9bd034..2be9821 100644 --- a/custom_components/leslies_pool/config_flow.py +++ b/custom_components/leslies_pool/config_flow.py @@ -1,16 +1,17 @@ """Config flow for Leslie's Pool Water Tests integration.""" - from __future__ import annotations import logging import re from typing import Any +import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -import voluptuous as vol from .api import LesliesPoolApi from .const import DOMAIN diff --git a/custom_components/leslies_pool/requirements.txt b/custom_components/leslies_pool/requirements.txt index ce98cb1..daca14b 100644 --- a/custom_components/leslies_pool/requirements.txt +++ b/custom_components/leslies_pool/requirements.txt @@ -1,2 +1,2 @@ beautifulsoup4==4.12.3 -requests==2.31.0 \ No newline at end of file +requests==2.31.0 diff --git a/custom_components/leslies_pool/sensor.py b/custom_components/leslies_pool/sensor.py index e605f62..c395706 100644 --- a/custom_components/leslies_pool/sensor.py +++ b/custom_components/leslies_pool/sensor.py @@ -1,15 +1,16 @@ """Sensor platform for Leslie's Pool Water Tests.""" - -from datetime import timedelta import logging +from datetime import timedelta +import requests from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -import requests +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import UpdateFailed from .const import DOMAIN diff --git a/requirements.txt b/requirements.txt index ce98cb1..daca14b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ beautifulsoup4==4.12.3 -requests==2.31.0 \ No newline at end of file +requests==2.31.0 diff --git a/tests/conftest.py b/tests/conftest.py index d4a5a4f..7670d34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ """Common fixtures for the Leslie's Pool Water Tests tests.""" - from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock +from unittest.mock import patch import pytest diff --git a/tests/const.py b/tests/const.py index 92cfc18..cb7c492 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,5 +1,5 @@ """Constants for leslies_pool tests.""" - -from custom_components.leslies_pool.const import CONF_PASSWORD, CONF_USERNAME +from custom_components.leslies_pool.const import CONF_PASSWORD +from custom_components.leslies_pool.const import CONF_USERNAME MOCK_CONFIG = {CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password"} diff --git a/tests/test_api.py b/tests/test_api.py index c8dec6c..7b7d4cf 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,7 @@ """Test the API for Leslie's Pool Water Tests.""" - import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock +from unittest.mock import patch from homeassistant.components.leslies_pool.api import LesliesPoolApi diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 96dede7..a1a16e2 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,15 +1,15 @@ """Test the Leslie's Pool Water Tests config flow.""" - -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock +from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components.leslies_pool.config_flow import ( - CannotConnect, - InvalidAuth, - InvalidURL, -) +from homeassistant.components.leslies_pool.config_flow import CannotConnect +from homeassistant.components.leslies_pool.config_flow import InvalidAuth +from homeassistant.components.leslies_pool.config_flow import InvalidURL from homeassistant.components.leslies_pool.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 93e13f7..6871f0f 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,16 +1,13 @@ """Test the Leslie's Pool Water Tests sensors.""" - -from datetime import timedelta import logging -from unittest.mock import AsyncMock, patch +from datetime import timedelta +from unittest.mock import AsyncMock +from unittest.mock import patch import pytest - from homeassistant.components.leslies_pool.const import DOMAIN -from homeassistant.components.leslies_pool.sensor import ( - LesliesPoolSensor, - async_setup_entry, -) +from homeassistant.components.leslies_pool.sensor import async_setup_entry +from homeassistant.components.leslies_pool.sensor import LesliesPoolSensor from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) From 68042f2403685d087a5db2a6a24a6e63a4481a87 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:32:50 -0400 Subject: [PATCH 07/13] fix black --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 511f9f0..b5a3c24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,7 @@ repos: language: system types: [python] require_serial: true + additional_dependencies: [black==22.3.0, click==8.0.4] - id: flake8 name: flake8 entry: flake8 From db4ed67e76d68a7d265d7c94625b489148f82074 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:34:13 -0400 Subject: [PATCH 08/13] addition black fix --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index daca14b..41a1708 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ beautifulsoup4==4.12.3 requests==2.31.0 +black==22.3.0 +click==8.0.4 From 4a550677dfbfd7b3dfbb381466fcaa2379786736 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:38:32 -0400 Subject: [PATCH 09/13] another attempt at fixing black --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5a3c24..11b09ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: black name: black entry: black - language: system + language: python types: [python] require_serial: true additional_dependencies: [black==22.3.0, click==8.0.4] From 505ca52f1af5866f305ecf6b91bc41b151747574 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:41:18 -0400 Subject: [PATCH 10/13] fix manifest ordering --- custom_components/leslies_pool/manifest.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/leslies_pool/manifest.json b/custom_components/leslies_pool/manifest.json index 77e58ae..203bcc5 100644 --- a/custom_components/leslies_pool/manifest.json +++ b/custom_components/leslies_pool/manifest.json @@ -1,12 +1,12 @@ { "domain": "leslies_pool", "name": "Leslie's Pool Water Tests", + "codeowners": ["@connorgallopo"], + "config_flow": true, + "dependencies": [], "documentation": "https://github.com/connorgallopo/leslies-pool", + "iot_class": "cloud_polling", "issue_tracker": "https://github.com/connorgallopo/leslies-pool/issues", - "dependencies": [], - "config_flow": true, - "codeowners": ["@connorgallopo"], "requirements": ["beautifulsoup4==4.12.3", "requests==2.31.0"], - "version": "1.0.0", - "iot_class": "cloud_polling" + "version": "1.0.0" } From 883a6f67729770af57e5f70a963019f323b14ba3 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:49:08 -0400 Subject: [PATCH 11/13] switch action --- .github/workflows/hassfest.yml | 14 -------------- .github/workflows/validate.yml | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 14 deletions(-) delete mode 100644 .github/workflows/hassfest.yml create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml deleted file mode 100644 index 574d2c4..0000000 --- a/.github/workflows/hassfest.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Validate with hassfest - -on: - push: - pull_request: - schedule: - - cron: "0 0 * * *" - -jobs: - validate: - runs-on: "ubuntu-latest" - steps: - - uses: "actions/checkout@v4" - - uses: "home-assistant/actions/hassfest@master" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..c422ec3 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,18 @@ +name: Validate + +on: + push: + pull_request: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + validate-hacs: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v3" + - name: HACS validation + uses: "hacs/action@main" + with: + category: "integration" From d70cf60de0761684751143b4c0763090aca35bf9 Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:52:29 -0400 Subject: [PATCH 12/13] fix keys --- hacs.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/hacs.json b/hacs.json index 6ad98ef..eb04793 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,5 @@ { "name": "Leslie's Pool Water Tests", "hacs": "1.6.0", - "domains": ["sensor"], - "iot_class": "Cloud Polling", "homeassistant": "0.118.0" } From 3aea8b93643f5caeff15fc69f8e74d7d010ebd1b Mon Sep 17 00:00:00 2001 From: Connor Gallopo Date: Mon, 10 Jun 2024 13:57:03 -0400 Subject: [PATCH 13/13] remove tests until rewrite of yml --- .github/workflows/tests.yaml | 85 ------------------------------------ 1 file changed, 85 deletions(-) delete mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 8099cf5..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,85 +0,0 @@ -name: Linting - -on: - push: - branches: - - main - - master - - dev - pull_request: - schedule: - - cron: "0 0 * * *" - -env: - DEFAULT_PYTHON: 3.9 - -jobs: - pre-commit: - runs-on: "ubuntu-latest" - name: Pre-commit - steps: - - name: Check out the repository - uses: actions/checkout@v2.3.4 - - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.2.1 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - - name: Upgrade pip - run: | - pip install --constraint=.github/workflows/constraints.txt pip - pip --version - - - name: Install Python modules - run: | - pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports - - - name: Run pre-commit on all files - run: | - pre-commit run --all-files --show-diff-on-failure --color=always - - hacs: - runs-on: "ubuntu-latest" - name: HACS - steps: - - name: Check out the repository - uses: "actions/checkout@v2.3.4" - - - name: HACS validation - uses: "hacs/action@20.12.0" - with: - category: "integration" - ignore: brands - - hassfest: - runs-on: "ubuntu-latest" - name: Hassfest - steps: - - name: Check out the repository - uses: "actions/checkout@v2.3.4" - - - name: Hassfest validation - uses: "home-assistant/actions/hassfest@master" - tests: - runs-on: "ubuntu-latest" - name: Run tests - steps: - - name: Check out code from GitHub - uses: "actions/checkout@v2.3.4" - - name: Setup Python ${{ env.DEFAULT_PYTHON }} - uses: "actions/setup-python@v2.2.1" - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Install requirements - run: | - pip install --constraint=.github/workflows/constraints.txt pip - pip install -r requirements_test.txt - - name: Tests suite - run: | - pytest \ - --timeout=9 \ - --durations=10 \ - -n auto \ - -p no:sugar \ - tests