Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions custom_components/myhome/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from OWNd.connection import OWNGateway, OWNSession
from OWNd.discovery import find_gateways, get_gateway
from voluptuous import In, Required
Expand Down Expand Up @@ -93,7 +94,9 @@ async def async_step_user(self, user_input=None): # type: ignore

try:
async with asyncio.timeout(5):
local_gateways = await find_gateways()
local_gateways = await find_gateways(
session=async_get_clientsession(self.hass)
)
except asyncio.TimeoutError:
return self.async_abort(reason="discovery_timeout")

Expand Down Expand Up @@ -200,8 +203,18 @@ async def async_step_custom(self, user_input=None, errors=None): # type: ignore
if not errors:
try:
async with asyncio.timeout(5):
discovered = await get_gateway(user_input["address"])
except (asyncio.TimeoutError, Exception): # pylint: disable=broad-except
discovered = await get_gateway(
user_input["address"],
session=async_get_clientsession(self.hass),
)
except Exception as err: # pylint: disable=broad-except
# UPnP lookup is optional: any failure (timeout, HTTP,
# malformed XML) falls back to manual MAC entry.
LOGGER.debug(
"UPnP lookup of %s failed, asking for the MAC: %r",
user_input["address"],
err,
)
discovered = None

if discovered is not None:
Expand Down
6 changes: 3 additions & 3 deletions custom_components/myhome/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ rules:
comment: ConfigFlow with SSDP discovery, manual entry, serial transport, reauth and reconfigure.
dependency-transparency:
status: done
comment: OWNd is published on PyPI from the public OpenWebNet-HA/OWNd repository with CI; MIT/GPL licensed.
comment: OWNd is published on PyPI from the public OpenWebNet-HA/OWNd repository with CI; Apache-2.0 licensed.
docs-actions:
status: done
comment: docs/configuration/services.md documents all eleven services and their fields (services.yaml, strings.json and icons.json list the same eleven).
Expand Down Expand Up @@ -171,8 +171,8 @@ rules:
status: done
comment: OWNd client library is 100% native non-blocking asyncio protocols and streams.
inject-websession:
status: exempt
comment: No HTTP is used; communication is via raw TCP OpenWebNet sockets.
status: done
comment: async_get_clientsession(self.hass) is passed to OWNd.discovery.find_gateways and get_gateway for UPnP/SOAP HTTP calls; OpenWebNet bus communication uses raw TCP sockets.
strict-typing:
status: done
comment: mypy --strict is ratcheted per module by scripts/typing_ratchet.py against mypy_baseline.json; 0 errors remain in all modules. OWNd ships py.typed (PEP 561) as of 2.0.0b7.
4 changes: 2 additions & 2 deletions docs/quality_scale_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ Add a parametrised test that asserts the table so it cannot regress.

## Phase 5 — 🏆 Platinum: `strict-typing`

- ✅ Ratchet in place: `[tool.mypy]` (strict) in `pyproject.toml`, `mypy_baseline.json` with a per-module ceiling, `scripts/typing_ratchet.py` (fails on any regression, `--update` only lowers), workflow `strict-typing.yml`. 15 of 28 modules are clean (`const`, `data`, `gateway`, `myhome_device`, `services`, `device_trigger`, `decoder_pool`, transports, diagnostics, repairs, bus monitor); **504 errors remain in 13 modules** — biggest first: `config_flow` 105, `cover` 64, `validate` 57, `binary_sensor` 43, `light` 37, `sensor` 32, `websocket` 30, `__init__` 27, `button` 27, `climate` 25, `media_player` 25, `alarm_control_panel` 18, `switch` 14 (`discovery` and `router` are clean). Each PR that touches a module should leave it lower and run `--update`.
- ✅ Ratchet in place: `[tool.mypy]` (strict) in `pyproject.toml`, `mypy_baseline.json` with a per-module ceiling, `scripts/typing_ratchet.py` (fails on any regression, `--update` only lowers), workflow `strict-typing.yml`. ✅ All 71 modules are clean: `mypy_baseline.json` holds `"total": 0` with no per-module ceilings, so any new `mypy --strict` error fails the job.
- ✅ OWNd ships `py.typed` (PEP 561). The `OWNd.*` `ignore_missing_imports` override is gone and `strict-typing.yml` installs the `manifest.json` `OWNd==` pin, so the ratchet checks the integration against the OWNd types users install.
- `inject-websession` is correctly `exempt` (raw TCP, no HTTP); `async-dependency` is done.
- ✅ `inject-websession`: config-flow discovery passes `async_get_clientsession(hass)` to `OWNd.discovery.find_gateways` / `get_gateway` for the UPnP/SOAP HTTP calls (the bus itself is raw TCP); `async-dependency` is done.

## Maintainability (not a scale rule, keeps the rules honest)

Expand Down
42 changes: 41 additions & 1 deletion tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,9 +594,14 @@ async def test_user_step_discovery_timeout(hass: HomeAssistant) -> None:
assert result["reason"] == "discovery_timeout"


async def test_custom_step_validation_and_timeout(hass: HomeAssistant) -> None:
async def test_custom_step_validation_and_timeout(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test custom step error on invalid IP and timeout fallback to custom_manual."""
import asyncio
import logging

caplog.set_level(logging.DEBUG, logger="custom_components.myhome")

with patch("custom_components.myhome.config_flow.find_gateways", return_value=[]):
result = await hass.config_entries.flow.async_init(
Expand Down Expand Up @@ -626,6 +631,7 @@ async def test_custom_step_validation_and_timeout(hass: HomeAssistant) -> None:
)
assert result_manual["type"] == FlowResultType.FORM
assert result_manual["step_id"] == "custom_manual"
assert "UPnP lookup of 192.168.1.50 failed, asking for the MAC: TimeoutError()" in caplog.text


async def test_custom_manual_step_invalid_inputs(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -2654,3 +2660,37 @@ async def test_options_flow_ignored_addresses(hass: HomeAssistant) -> None:
assert result_comma["type"] == FlowResultType.CREATE_ENTRY
assert entry.options[CONF_IGNORED_ADDRESSES] == []


@pytest.mark.asyncio
async def test_config_flow_injects_ha_websession_into_discovery(hass: HomeAssistant):
"""Verify async_get_clientsession(hass) is passed to find_gateways and get_gateway."""
from homeassistant.helpers.aiohttp_client import async_get_clientsession

expected_session = async_get_clientsession(hass)
with patch(
"custom_components.myhome.config_flow.find_gateways",
new_callable=AsyncMock,
return_value=[],
) as mock_find, patch(
"custom_components.myhome.config_flow.get_gateway",
new_callable=AsyncMock,
return_value=None,
) as mock_get:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == FlowResultType.FORM
mock_find.assert_awaited_once_with(session=expected_session)

result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"serial": "00:00:00:00:00:00"}
)
assert result2["step_id"] == "custom"

result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"address": "192.168.1.99", "port": 20000},
)
assert result3["step_id"] == "custom_manual"
mock_get.assert_awaited_once_with("192.168.1.99", session=expected_session)

Loading