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
42 changes: 38 additions & 4 deletions homeassistant/components/bsblan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@
)
from homeassistant.helpers.typing import ConfigType

from .const import CONF_HEATING_CIRCUITS, CONF_PASSKEY, DEFAULT_PORT, DOMAIN, LOGGER
from .const import (
CONF_HEATING_CIRCUITS,
CONF_PASSKEY,
DEFAULT_HEATING_CIRCUITS,
DEFAULT_PORT,
DOMAIN,
LOGGER,
)
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
from .services import async_setup_services

Expand Down Expand Up @@ -118,7 +125,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo

# Read available heating circuits from config entry data
# (populated by config flow or migration)
circuits: list[int] = entry.data[CONF_HEATING_CIRCUITS]
circuits: list[int] = entry.data[CONF_HEATING_CIRCUITS] or list(
DEFAULT_HEATING_CIRCUITS
)

# Fetch required device metadata in parallel for faster startup
device, info = await asyncio.gather(
Expand Down Expand Up @@ -229,7 +238,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
# heating circuits from the device; fall back to [1] (pre-multi-circuit
# default) if the device is unreachable or the endpoint is unsupported.
if entry.version == 1 and entry.minor_version < 2:
circuits: list[int] = [1]
circuits: list[int] = list(DEFAULT_HEATING_CIRCUITS)
config = BSBLANConfig(
host=entry.data[CONF_HOST],
passkey=entry.data[CONF_PASSKEY],
Expand All @@ -245,11 +254,18 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
except (BSBLANError, TimeoutError) as err:
LOGGER.warning(
"Circuit discovery during migration failed for %s (%s); "
"defaulting to single circuit [1]. Use Reconfigure to "
"defaulting to a single circuit. Use Reconfigure to "
"rediscover additional circuits later",
entry.data[CONF_HOST],
err,
)
if not circuits:
LOGGER.warning(
"Circuit discovery during migration returned no heating circuits "
"for %s; defaulting to a single circuit",
entry.data[CONF_HOST],
Comment thread
liudger marked this conversation as resolved.
)
circuits = list(DEFAULT_HEATING_CIRCUITS)

hass.config_entries.async_update_entry(
entry,
Expand All @@ -263,4 +279,22 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
circuits,
)

# 1.2 -> 1.3: Repair entries that stored an empty circuit list during
# discovery. Every BSB-LAN setup has at least one heating circuit.
if entry.version == 1 and entry.minor_version < 3:
if not entry.data[CONF_HEATING_CIRCUITS]:
LOGGER.warning(
"Stored heating circuits for %s are empty; defaulting to a "
"single circuit",
entry.data[CONF_HOST],
Comment thread
liudger marked this conversation as resolved.
)
data = {
**entry.data,
CONF_HEATING_CIRCUITS: list(DEFAULT_HEATING_CIRCUITS),
}
else:
data = {**entry.data}

hass.config_entries.async_update_entry(entry, data=data, minor_version=3)

return True
22 changes: 18 additions & 4 deletions homeassistant/components/bsblan/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,28 @@
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo

from .const import CONF_HEATING_CIRCUITS, CONF_PASSKEY, DEFAULT_PORT, DOMAIN, LOGGER
from .const import (
CONF_HEATING_CIRCUITS,
CONF_PASSKEY,
DEFAULT_HEATING_CIRCUITS,
DEFAULT_PORT,
DOMAIN,
LOGGER,
)


class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a BSBLAN config flow."""

VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3

def __init__(self) -> None:
"""Initialize BSBLan flow."""
self.host: str = ""
self.port: int = DEFAULT_PORT
self.mac: str | None = None
self.circuits: list[int] = [1]
self.circuits: list[int] = list(DEFAULT_HEATING_CIRCUITS)
self.passkey: str | None = None
self.username: str | None = None
self.password: str | None = None
Expand Down Expand Up @@ -384,6 +391,13 @@ async def _discover_circuits(self) -> None:
try:
await bsblan.initialize()
self.circuits = await bsblan.get_available_circuits()
if not self.circuits:
LOGGER.debug(
"Circuit discovery returned no heating circuits for %s, "
"defaulting to single circuit",
self.host,
)
self.circuits = list(DEFAULT_HEATING_CIRCUITS)
except (
BSBLANError,
TimeoutError,
Expand All @@ -392,4 +406,4 @@ async def _discover_circuits(self) -> None:
"Circuit discovery not available for %s, defaulting to single circuit",
self.host,
)
self.circuits = [1]
self.circuits = list(DEFAULT_HEATING_CIRCUITS)
1 change: 1 addition & 0 deletions homeassistant/components/bsblan/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@
CONF_PASSKEY: Final = "passkey"
CONF_HEATING_CIRCUITS: Final = "heating_circuits"

DEFAULT_HEATING_CIRCUITS: Final = (1,)
DEFAULT_PORT: Final = 80
4 changes: 2 additions & 2 deletions tests/components/bsblan/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def mock_config_entry() -> MockConfigEntry:
},
unique_id="00:80:41:19:69:90",
version=1,
minor_version=2,
minor_version=3,
)


Expand All @@ -61,7 +61,7 @@ def mock_config_entry_dual_circuit() -> MockConfigEntry:
},
unique_id="00:80:41:19:69:90",
version=1,
minor_version=2,
minor_version=3,
)


Expand Down
70 changes: 70 additions & 0 deletions tests/components/bsblan/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,44 @@ async def test_circuit_discovery_failure_falls_back_to_default(
)


async def test_circuit_discovery_empty_result_falls_back_to_default(
hass: HomeAssistant,
mock_bsblan: MagicMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test that empty circuit discovery falls back to single circuit."""
mock_bsblan.get_available_circuits.return_value = []

result = await _init_user_flow(hass)
_assert_form_result(result, "user")

result = await _configure_flow(
hass,
result["flow_id"],
{
CONF_HOST: "127.0.0.1",
CONF_PORT: 80,
CONF_PASSKEY: "1234",
CONF_USERNAME: "admin",
CONF_PASSWORD: "admin1234",
},
)

_assert_create_entry_result(
result,
"BSB-LAN",
{
CONF_HOST: "127.0.0.1",
CONF_PORT: 80,
CONF_PASSKEY: "1234",
CONF_USERNAME: "admin",
CONF_PASSWORD: "admin1234",
CONF_HEATING_CIRCUITS: [1],
},
format_mac("00:80:41:19:69:90"),
)


async def test_connection_error(
hass: HomeAssistant,
mock_bsblan: MagicMock,
Expand Down Expand Up @@ -1150,6 +1188,38 @@ async def test_reconfigure_flow_success(
assert mock_config_entry.data[CONF_HEATING_CIRCUITS] == [1]


async def test_reconfigure_flow_empty_circuit_discovery_falls_back(
hass: HomeAssistant,
mock_bsblan: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test reconfigure stores single circuit when discovery returns no circuits."""
mock_config_entry.add_to_hass(hass)
mock_bsblan.get_available_circuits.return_value = []

result = await mock_config_entry.start_reconfigure_flow(hass)

_assert_form_result(result, "reconfigure")

result = await _configure_flow(
hass,
result["flow_id"],
{
CONF_HOST: "192.168.1.50",
CONF_PORT: 8080,
CONF_PASSKEY: "new_passkey",
CONF_USERNAME: "new_admin",
CONF_PASSWORD: "new_password",
},
)

_assert_abort_result(result, "reconfigure_successful")

assert mock_config_entry.data[CONF_HOST] == "192.168.1.50"
assert mock_config_entry.data[CONF_PORT] == 8080
assert mock_config_entry.data[CONF_HEATING_CIRCUITS] == [1]


@pytest.mark.parametrize(
("side_effect", "error"),
[
Expand Down
56 changes: 53 additions & 3 deletions tests/components/bsblan/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,35 @@ async def test_migrate_entry_discovers_circuits(

assert entry.state is ConfigEntryState.LOADED
assert entry.version == 1
assert entry.minor_version == 2
assert entry.minor_version == 3
assert entry.data[CONF_HEATING_CIRCUITS] == [1, 2]


async def test_migrate_entry_empty_discovery_falls_back(
hass: HomeAssistant,
mock_bsblan: MagicMock,
) -> None:
"""Test migration falls back to [1] when discovery returns no circuits."""
mock_bsblan.get_available_circuits.return_value = []

entry = MockConfigEntry(
title="BSBLAN Setup",
domain=DOMAIN,
data=_legacy_entry_data(),
unique_id="00:80:41:19:69:90",
version=1,
minor_version=1,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

assert entry.state is ConfigEntryState.LOADED
assert entry.version == 1
assert entry.minor_version == 3
assert entry.data[CONF_HEATING_CIRCUITS] == [1]


async def test_migrate_entry_discovery_failure_falls_back(
hass: HomeAssistant,
mock_bsblan: MagicMock,
Expand All @@ -398,7 +423,7 @@ async def test_migrate_entry_discovery_failure_falls_back(

assert entry.state is ConfigEntryState.LOADED
assert entry.version == 1
assert entry.minor_version == 2
assert entry.minor_version == 3
assert entry.data[CONF_HEATING_CIRCUITS] == [1]


Expand All @@ -422,8 +447,33 @@ async def test_migrate_entry_discovery_timeout_falls_back(
await hass.async_block_till_done()

assert entry.state is ConfigEntryState.LOADED
assert entry.minor_version == 2
assert entry.minor_version == 3
assert entry.data[CONF_HEATING_CIRCUITS] == [1]


async def test_migrate_entry_stored_empty_circuits_falls_back(
hass: HomeAssistant,
mock_bsblan: MagicMock,
) -> None:
"""Test migration repairs stored empty heating circuits."""
entry = MockConfigEntry(
title="BSBLAN Setup",
domain=DOMAIN,
data={**_legacy_entry_data(), CONF_HEATING_CIRCUITS: []},
unique_id="00:80:41:19:69:90",
version=1,
minor_version=2,
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

assert entry.state is ConfigEntryState.LOADED
assert entry.version == 1
assert entry.minor_version == 3
assert entry.data[CONF_HEATING_CIRCUITS] == [1]
assert entry.runtime_data.available_circuits == [1]
assert mock_bsblan.get_available_circuits.call_count == 0


async def test_migrate_entry_future_version_aborts(
Expand Down
Loading