From f7102fd85f869548009f11c0813d2c0d0f60b437 Mon Sep 17 00:00:00 2001 From: Jon Connell Date: Thu, 19 Oct 2023 16:21:39 +0100 Subject: [PATCH] Added debug option for zeep transport --- Changelog.md | 4 ++ .../kingspan_watchman_sensit/api.py | 21 ++++--- .../kingspan_watchman_sensit/config_flow.py | 15 ++--- .../kingspan_watchman_sensit/const.py | 1 + .../translations/en.json | 3 +- tests/conftest.py | 37 +++++------- tests/const.py | 3 +- tests/test_config_flow.py | 59 ++++++++++++++----- 8 files changed, 87 insertions(+), 56 deletions(-) diff --git a/Changelog.md b/Changelog.md index 23e4154..8036767 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,10 @@ 🪲 indicates bug fixes 🚀 indicates new features or improvements +## v1.6.0 + +🚀 Configuration option added for debugging Kingspan connections. When enabled, very verbose logs are generated for the connection to the Kingspan internet service. The logs include username and password. + ## v1.5.0 🚀 The integration now supports an options flow for configuring parameters. Currently supported parameters are the update interval (default is 8 hours) and the number of days to consider for average usage (default is 14 days). You can change these by clicking **Configure** from the integration's entry in **Settings > Devices & Services**. diff --git a/custom_components/kingspan_watchman_sensit/api.py b/custom_components/kingspan_watchman_sensit/api.py index 4a41439..0bd58c7 100644 --- a/custom_components/kingspan_watchman_sensit/api.py +++ b/custom_components/kingspan_watchman_sensit/api.py @@ -6,7 +6,7 @@ from async_timeout import timeout from connectsensor import APIError, AsyncSensorClient -from .const import API_TIMEOUT, REFILL_THRESHOLD, DEFAULT_USAGE_WINDOW +from .const import API_TIMEOUT, DEFAULT_USAGE_WINDOW, REFILL_THRESHOLD _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -20,13 +20,20 @@ def __init__(self): class SENSiTApiClient: def __init__( - self, username: str, password: str, usage_window: int = DEFAULT_USAGE_WINDOW + self, + username: str, + password: str, + usage_window: int = DEFAULT_USAGE_WINDOW, + debug=False, ) -> None: """Simple API Client for .""" _LOGGER.debug("API init as username=%s", username) self._username = username self._password = password self._usage_window = usage_window + if debug: + _LOGGER.debug("Enabling Zeep service debug") + _LOGGER.debug("Logger = %s", logging.getLogger("zeep.transports")) async def async_get_data(self) -> dict: """Get tank data from the API""" @@ -92,12 +99,9 @@ def usage_rate(self, tank_data: TankData): delta_levels = [] current_level = history[0]["level_litres"] - for index, row in enumerate(history[1:]): + for _, row in enumerate(history[1:]): # Ignore refill days where oil goes up significantly - if ( - current_level != 0 - and (row["level_litres"] / current_level) < REFILL_THRESHOLD - ): + if current_level != 0 and (row["level_litres"] / current_level) < REFILL_THRESHOLD: delta_levels.append(current_level - row["level_litres"]) current_level = row["level_litres"] @@ -127,8 +131,7 @@ def filter_history(history: list[dict], usage_window) -> list[dict]: time_delta = time_delta.replace(tzinfo=LOCAL_TZINFO) # API returns naive datetime rather than with timezones history = [ - dict(x, reading_date=x["reading_date"].replace(tzinfo=LOCAL_TZINFO)) - for x in history + dict(x, reading_date=x["reading_date"].replace(tzinfo=LOCAL_TZINFO)) for x in history ] history = [x for x in history if x["reading_date"] >= time_delta] return history diff --git a/custom_components/kingspan_watchman_sensit/config_flow.py b/custom_components/kingspan_watchman_sensit/config_flow.py index 4ab1a5d..9c04271 100644 --- a/custom_components/kingspan_watchman_sensit/config_flow.py +++ b/custom_components/kingspan_watchman_sensit/config_flow.py @@ -1,20 +1,16 @@ """Adds config flow for Kingspan Watchman SENSiT.""" import logging -from typing import Any, Dict, Optional, Mapping +from typing import Dict import homeassistant.helpers.config_validation as cv import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity_registry import ( - async_entries_for_config_entry, - async_get, -) from .api import SENSiTApiClient from .const import ( + CONF_KINGSPAN_DEBUG, CONF_NAME, CONF_PASSWORD, CONF_UPDATE_INTERVAL, @@ -114,6 +110,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: self.options = dict(config_entry.options) self.options.setdefault(CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL) self.options.setdefault(CONF_USAGE_WINDOW, DEFAULT_USAGE_WINDOW) + self.options.setdefault(CONF_KINGSPAN_DEBUG, False) async def async_step_init(self, user_input: dict | None = None) -> FlowResult: """Initialise the options flow""" @@ -139,6 +136,10 @@ async def async_step_init(self, user_input: dict | None = None) -> FlowResult: CONF_USAGE_WINDOW, DEFAULT_USAGE_WINDOW ), ): cv.positive_int, + vol.Optional( + CONF_KINGSPAN_DEBUG, + default=self.config_entry.options.get(CONF_KINGSPAN_DEBUG, False), + ): cv.boolean, } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/custom_components/kingspan_watchman_sensit/const.py b/custom_components/kingspan_watchman_sensit/const.py index a79eeee..a5274fb 100644 --- a/custom_components/kingspan_watchman_sensit/const.py +++ b/custom_components/kingspan_watchman_sensit/const.py @@ -17,6 +17,7 @@ CONF_NAME = "name" CONF_USAGE_WINDOW = "usage_window" CONF_UPDATE_INTERVAL = "update_interval" +CONF_KINGSPAN_DEBUG = "debug_kingspan" # Defaults DEFAULT_TANK_NAME = "My Tank" diff --git a/custom_components/kingspan_watchman_sensit/translations/en.json b/custom_components/kingspan_watchman_sensit/translations/en.json index 0d23e03..ecd2b99 100644 --- a/custom_components/kingspan_watchman_sensit/translations/en.json +++ b/custom_components/kingspan_watchman_sensit/translations/en.json @@ -32,7 +32,8 @@ "description": "Configure options for Kingspan Watchman SENSiT", "data": { "update_interval": "How often to refresh the tank data (hours)", - "usage_window": "Period to consider for average usage (days)" + "usage_window": "Period to consider for average usage (days)", + "debug_kingspan": "Enable verbose debug of Kingspan service connection (warning: exposes password in logfile)" } } } diff --git a/tests/conftest.py b/tests/conftest.py index b911004..4c3fdb3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,20 @@ """Global fixtures for Kingspan Watchman SENSiT integration.""" -import pytest_asyncio +from datetime import datetime, timedelta +from unittest.mock import AsyncMock, patch +import pytest_asyncio from async_property import async_property -from datetime import datetime, timedelta, timezone -from unittest.mock import patch, AsyncMock from connectsensor import APIError from .const import ( + MOCK_TANK_CAPACITY, MOCK_TANK_LEVEL, - MOCK_TANK_SERIAL_NUMBER, MOCK_TANK_MODEL, MOCK_TANK_NAME, - MOCK_TANK_CAPACITY, + MOCK_TANK_SERIAL_NUMBER, HistoryType, ) - pytest_plugins = "pytest_homeassistant_custom_component" @@ -42,9 +41,7 @@ def skip_notifications_fixture(): @pytest_asyncio.fixture(name="bypass_get_data") def bypass_get_data_fixture(): """Skip calls to get data from API.""" - with patch( - "custom_components.kingspan_watchman_sensit.SENSiTApiClient.async_get_data" - ), patch( + with patch("custom_components.kingspan_watchman_sensit.SENSiTApiClient.async_get_data"), patch( "custom_components.kingspan_watchman_sensit.SENSiTApiClient.check_credentials" ): yield @@ -58,9 +55,7 @@ def error_get_data_fixture(): with patch( "custom_components.kingspan_watchman_sensit.SENSiTApiClient.async_get_data", side_effect=Exception, - ), patch( - "custom_components.kingspan_watchman_sensit.SENSiTApiClient.check_credentials" - ): + ), patch("custom_components.kingspan_watchman_sensit.SENSiTApiClient.check_credentials"): yield @@ -70,7 +65,7 @@ def error_sensor_client_fixture(): with patch( "custom_components.kingspan_watchman_sensit.SENSiTApiClient.check_credentials", side_effect=APIError, - ) as mock_client: + ): yield @@ -80,15 +75,13 @@ def timeout_sensor_client_fixture(): with patch( "custom_components.kingspan_watchman_sensit.SENSiTApiClient.check_credentials", side_effect=TimeoutError, - ) as mock_client: + ): yield def decreasing_history(start_date: datetime) -> list: history = [] - start_date = start_date.replace( - hour=0, minute=30, second=0, microsecond=0 - ) - timedelta(days=30) + start_date = start_date.replace(hour=0, minute=30, second=0, microsecond=0) - timedelta(days=30) for day in range(1, 20): percent = 100 - (day * 4) @@ -197,9 +190,7 @@ def __init__(self, *args, **kwargs): @async_property async def tanks(self): if self._num_tanks == 1: - return [ - MockAsyncTank(tank_level=self._level, history_type=self._history_type) - ] + return [MockAsyncTank(tank_level=self._level, history_type=self._history_type)] else: return [ MockAsyncTank( @@ -215,13 +206,13 @@ async def tanks(self): def mock_sensor_client(request): """Replace the AsyncSensorClient with a mock context manager""" num_tanks = None - if type(request.param) == list and len(request.param) == 1: + if isinstance(request.param, list) and len(request.param) == 1: tank_level = request.param[0] history_type = HistoryType.DECREASING - elif type(request.param) == list and len(request.param) == 2: + elif isinstance(request.param, list) and len(request.param) == 2: tank_level = request.param[0] history_type = request.param[1] - elif type(request.param) == list and len(request.param) == 3: + elif isinstance(request.param, list) and len(request.param) == 3: tank_level = request.param[0] history_type = request.param[1] num_tanks = request.param[2] diff --git a/tests/const.py b/tests/const.py index c64aab1..8961ab9 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,10 +1,11 @@ """Constants for Kingspan Watchman SENSiT tests.""" from enum import Enum + from custom_components.kingspan_watchman_sensit.const import ( + CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - CONF_NAME, ) MOCK_CONFIG = { diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 87944d6..bba4153 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,14 +1,16 @@ """Test Kingspan Watchman SENSiT config flow.""" from unittest.mock import patch -import pytest import pytest_asyncio +from custom_components.kingspan_watchman_sensit import ( + async_setup_entry, + async_unload_entry, +) +from custom_components.kingspan_watchman_sensit.const import DOMAIN from homeassistant import config_entries, data_entry_flow from pytest_homeassistant_custom_component.common import MockConfigEntry -from custom_components.kingspan_watchman_sensit.const import DOMAIN - -from .const import MOCK_CONFIG, CONF_PASSWORD +from .const import CONF_PASSWORD, MOCK_CONFIG # This fixture bypasses the actual setup of the integration @@ -70,26 +72,55 @@ async def test_failed_config_flow(hass, error_on_get_data): assert result["errors"] == {"base": "auth"} -async def test_options_flow(hass): +async def test_options_default_flow(hass): """Test an options flow.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - entry.add_to_hass(hass) + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") + config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_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"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"update_interval": 2, "usage_window": 10} + result["flow_id"], + user_input={}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Mock Title" - assert entry.options == {"update_interval": 2, "usage_window": 10} + assert config_entry.options == { + "debug_kingspan": False, + "update_interval": 8, + "usage_window": 14, + } + + +async def test_options_flow(hass, bypass_get_data): + """Test an options flow.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"debug_kingspan": True, "update_interval": 4, "usage_window": 28}, + ) + + assert await async_setup_entry(hass, config_entry) + + assert config_entry.options == { + "debug_kingspan": True, + "update_interval": 4, + "usage_window": 28, + } + + assert await async_unload_entry(hass, config_entry) # Re-auth test Copyright (c) 2020 Joakim Sørensen @ludeeus @@ -108,9 +139,7 @@ async def test_reauth_config_flow(hass, bypass_get_data): assert result["step_id"] == "reauth_confirm" # If a user were to confirm the re-auth start, this function call - result_2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + result_2 = await hass.config_entries.flow.async_configure(result["flow_id"], user_input={}) # It should load the user form assert result_2["type"] == data_entry_flow.RESULT_TYPE_FORM