Skip to content

Commit

Permalink
Added debug option for zeep transport
Browse files Browse the repository at this point in the history
  • Loading branch information
masaccio committed Oct 19, 2023
1 parent f7507d6 commit f7102fd
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 56 deletions.
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down
21 changes: 12 additions & 9 deletions custom_components/kingspan_watchman_sensit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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"""
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
15 changes: 8 additions & 7 deletions custom_components/kingspan_watchman_sensit/config_flow.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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"""
Expand All @@ -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))
1 change: 1 addition & 0 deletions custom_components/kingspan_watchman_sensit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
}
Expand Down
37 changes: 14 additions & 23 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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"


Expand All @@ -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
Expand All @@ -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


Expand All @@ -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


Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion tests/const.py
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down
59 changes: 44 additions & 15 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit f7102fd

Please sign in to comment.