mirror of
https://github.com/home-assistant/core.git
synced 2026-08-15 01:32:55 +01:00
570 lines
18 KiB
Python
570 lines
18 KiB
Python
"""Test the DSMR config flow."""
|
|
|
|
from itertools import chain, repeat
|
|
from typing import Any
|
|
from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch
|
|
|
|
from dsmr_parser.exceptions import DecryptionError
|
|
import pytest
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.components.dsmr.config_flow import CannotCommunicate
|
|
from homeassistant.components.dsmr.const import DOMAIN
|
|
from homeassistant.components.usb import SerialDevice
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.data_entry_flow import FlowResultType
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"}
|
|
SERIAL_DATA_SWEDEN = {"serial_id": None, "serial_id_gas": None}
|
|
|
|
|
|
def com_port() -> SerialDevice:
|
|
"""Mock of a serial port."""
|
|
return SerialDevice(
|
|
device="/dev/ttyUSB1234",
|
|
serial_number="1234",
|
|
manufacturer="Virtual serial port",
|
|
description="Some serial port",
|
|
)
|
|
|
|
|
|
async def test_setup_network(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
) -> None:
|
|
"""Test we can setup a network connection via a socket URL."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {}
|
|
|
|
with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{
|
|
"port": "socket://10.10.0.1:1234",
|
|
"dsmr_version": "2.2",
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
entry_data = {
|
|
"port": "socket://10.10.0.1:1234",
|
|
"dsmr_version": "2.2",
|
|
"protocol": "dsmr_protocol",
|
|
}
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "socket://10.10.0.1:1234"
|
|
assert result["data"] == {**entry_data, **SERIAL_DATA}
|
|
|
|
|
|
async def test_setup_network_rfxtrx(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
rfxtrx_dsmr_connection_send_validate_fixture: tuple[
|
|
MagicMock, MagicMock, MagicMock
|
|
],
|
|
) -> None:
|
|
"""Test we can setup a network connection via a socket URL for rfxtrx."""
|
|
(_connection_factory, _transport, protocol) = dsmr_connection_send_validate_fixture
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {}
|
|
|
|
# set-up DSMRProtocol to yield no valid telegram,
|
|
# this will retry with RFXtrxDSMRProtocol
|
|
protocol.telegram = {}
|
|
|
|
with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{
|
|
"port": "socket://10.10.0.1:1234",
|
|
"dsmr_version": "2.2",
|
|
},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
entry_data = {
|
|
"port": "socket://10.10.0.1:1234",
|
|
"dsmr_version": "2.2",
|
|
"protocol": "rfxtrx_dsmr_protocol",
|
|
}
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "socket://10.10.0.1:1234"
|
|
assert result["data"] == {**entry_data, **SERIAL_DATA}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("version", "entry_data"),
|
|
[
|
|
(
|
|
"2.2",
|
|
{
|
|
"port": "/dev/ttyUSB1234",
|
|
"dsmr_version": "2.2",
|
|
"protocol": "dsmr_protocol",
|
|
"serial_id": "12345678",
|
|
"serial_id_gas": "123456789",
|
|
},
|
|
),
|
|
(
|
|
"5B",
|
|
{
|
|
"port": "/dev/ttyUSB1234",
|
|
"dsmr_version": "5B",
|
|
"protocol": "dsmr_protocol",
|
|
"serial_id": "12345678",
|
|
"serial_id_gas": "123456789",
|
|
},
|
|
),
|
|
(
|
|
"5L",
|
|
{
|
|
"port": "/dev/ttyUSB1234",
|
|
"dsmr_version": "5L",
|
|
"protocol": "dsmr_protocol",
|
|
"serial_id": "12345678",
|
|
"serial_id_gas": "123456789",
|
|
},
|
|
),
|
|
(
|
|
"5EONHU",
|
|
{
|
|
"port": "/dev/ttyUSB1234",
|
|
"dsmr_version": "5EONHU",
|
|
"protocol": "dsmr_protocol",
|
|
"serial_id": "12345678",
|
|
"serial_id_gas": None,
|
|
},
|
|
),
|
|
(
|
|
"5S",
|
|
{
|
|
"port": "/dev/ttyUSB1234",
|
|
"dsmr_version": "5S",
|
|
"protocol": "dsmr_protocol",
|
|
"serial_id": None,
|
|
"serial_id_gas": None,
|
|
},
|
|
),
|
|
(
|
|
"Q3D",
|
|
{
|
|
"port": "/dev/ttyUSB1234",
|
|
"dsmr_version": "Q3D",
|
|
"protocol": "dsmr_protocol",
|
|
"serial_id": "12345678",
|
|
"serial_id_gas": None,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
async def test_setup_serial(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
version: str,
|
|
entry_data: dict[str, Any],
|
|
) -> None:
|
|
"""Test we can setup serial."""
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {}
|
|
|
|
with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": version},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == port.device
|
|
assert result["data"] == entry_data
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("version", "serial_data"),
|
|
[
|
|
("MSn", SERIAL_DATA),
|
|
("SAGEMCOM_T210_D_R", SERIAL_DATA_SWEDEN),
|
|
],
|
|
)
|
|
async def test_setup_serial_encrypted(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
version: str,
|
|
serial_data: dict[str, str | None],
|
|
) -> None:
|
|
"""Test we can setup an encrypted meter that asks for an encryption key."""
|
|
(connection_factory, _transport, _protocol) = dsmr_connection_send_validate_fixture
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": version},
|
|
)
|
|
|
|
# An encrypted version asks for the encryption key in a second step
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "encryption_key"
|
|
|
|
with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"encryption_key": "aabbccddeeff00112233445566778899"},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == port.device
|
|
assert result["data"] == {
|
|
"port": port.device,
|
|
"dsmr_version": version,
|
|
"protocol": "dsmr_protocol",
|
|
"encryption_key": "aabbccddeeff00112233445566778899",
|
|
**serial_data,
|
|
}
|
|
# The key is decrypted without verifying the GCM authentication tag
|
|
assert (
|
|
connection_factory.call_args.kwargs["encryption_key"]
|
|
== "aabbccddeeff00112233445566778899"
|
|
)
|
|
assert connection_factory.call_args.kwargs["authentication_key"] is None
|
|
|
|
|
|
async def test_setup_serial_encrypted_invalid_key(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
) -> None:
|
|
"""Test an encrypted meter with a wrong encryption key reports an error."""
|
|
(_connection_factory, _transport, protocol) = dsmr_connection_send_validate_fixture
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": "MSn"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "encryption_key"
|
|
|
|
# A wrong key makes the protocol report a decryption error
|
|
protocol.decryption_error = DecryptionError("wrong key")
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"encryption_key": "00000000000000000000000000000000"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "encryption_key"
|
|
assert result["errors"] == {"base": "invalid_key"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"encryption_key",
|
|
[
|
|
"tooshort",
|
|
"nothexnothexnothexnothexnothexgg",
|
|
"aabbccddeeff00112233445566778899ff",
|
|
],
|
|
ids=["too_short", "non_hex", "too_long"],
|
|
)
|
|
async def test_setup_serial_encrypted_malformed_key(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
encryption_key: str,
|
|
) -> None:
|
|
"""Test a malformed encryption key is rejected without a connection attempt."""
|
|
(connection_factory, _transport, _protocol) = dsmr_connection_send_validate_fixture
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": "MSn"},
|
|
)
|
|
|
|
assert result["step_id"] == "encryption_key"
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"encryption_key": encryption_key},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "encryption_key"
|
|
assert result["errors"] == {"base": "invalid_key"}
|
|
# A malformed key must not reach the reader
|
|
connection_factory.assert_not_called()
|
|
|
|
|
|
@pytest.mark.usefixtures("dsmr_connection_send_validate_fixture")
|
|
async def test_setup_serial_encrypted_cannot_communicate(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test an encrypted meter does not fall back to RFXtrx when it stays silent."""
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": "MSn"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "encryption_key"
|
|
|
|
with patch(
|
|
"homeassistant.components.dsmr.config_flow._validate_dsmr_connection",
|
|
side_effect=CannotCommunicate,
|
|
) as validate:
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"encryption_key": "aabbccddeeff00112233445566778899"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "encryption_key"
|
|
assert result["errors"] == {"base": "cannot_communicate"}
|
|
# Encrypted meters must not retry over the RFXtrx protocol
|
|
assert validate.call_count == 1
|
|
|
|
|
|
async def test_setup_serial_rfxtrx(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
rfxtrx_dsmr_connection_send_validate_fixture: tuple[
|
|
MagicMock, MagicMock, MagicMock
|
|
],
|
|
) -> None:
|
|
"""Test we can setup serial."""
|
|
(_connection_factory, _transport, protocol) = dsmr_connection_send_validate_fixture
|
|
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {}
|
|
|
|
# set-up DSMRProtocol to yield no valid telegram,
|
|
# this will retry with RFXtrxDSMRProtocol
|
|
protocol.telegram = {}
|
|
|
|
with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": "2.2"},
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
entry_data = {
|
|
"port": port.device,
|
|
"dsmr_version": "2.2",
|
|
"protocol": "rfxtrx_dsmr_protocol",
|
|
}
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == port.device
|
|
assert result["data"] == {**entry_data, **SERIAL_DATA}
|
|
|
|
|
|
async def test_setup_serial_fail(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
) -> None:
|
|
"""Test failed serial connection."""
|
|
(_connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture
|
|
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
# override the mock to have it fail the first time and succeed after
|
|
first_fail_connection_factory = AsyncMock(
|
|
return_value=(transport, protocol),
|
|
side_effect=chain([OSError], repeat(DEFAULT)),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {}
|
|
|
|
with patch(
|
|
"homeassistant.components.dsmr.config_flow.create_dsmr_reader",
|
|
first_fail_connection_factory,
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": "2.2"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
|
|
|
|
async def test_setup_serial_timeout(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
rfxtrx_dsmr_connection_send_validate_fixture: tuple[
|
|
MagicMock, MagicMock, MagicMock
|
|
],
|
|
) -> None:
|
|
"""Test failed serial connection."""
|
|
(_connection_factory, _transport, protocol) = dsmr_connection_send_validate_fixture
|
|
(
|
|
_connection_factory,
|
|
_transport,
|
|
rfxtrx_protocol,
|
|
) = rfxtrx_dsmr_connection_send_validate_fixture
|
|
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
first_timeout_wait_closed = AsyncMock(
|
|
return_value=True,
|
|
side_effect=chain([TimeoutError], repeat(DEFAULT)),
|
|
)
|
|
protocol.wait_closed = first_timeout_wait_closed
|
|
|
|
first_timeout_wait_closed = AsyncMock(
|
|
return_value=True,
|
|
side_effect=chain([TimeoutError], repeat(DEFAULT)),
|
|
)
|
|
rfxtrx_protocol.wait_closed = first_timeout_wait_closed
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {}
|
|
|
|
with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"port": port.device, "dsmr_version": "2.2"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {"base": "cannot_communicate"}
|
|
|
|
|
|
async def test_setup_serial_wrong_telegram(
|
|
hass: HomeAssistant,
|
|
dsmr_connection_send_validate_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
|
rfxtrx_dsmr_connection_send_validate_fixture: tuple[
|
|
MagicMock, MagicMock, MagicMock
|
|
],
|
|
) -> None:
|
|
"""Test failed telegram data."""
|
|
(_connection_factory, _transport, protocol) = dsmr_connection_send_validate_fixture
|
|
(
|
|
_rfxtrx_connection_factory,
|
|
_transport,
|
|
rfxtrx_protocol,
|
|
) = rfxtrx_dsmr_connection_send_validate_fixture
|
|
|
|
port = com_port()
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {}
|
|
|
|
protocol.telegram = {}
|
|
rfxtrx_protocol.telegram = {}
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{"port": port.device, "dsmr_version": "2.2"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "user"
|
|
assert result["errors"] == {"base": "cannot_communicate"}
|
|
|
|
|
|
async def test_options_flow(hass: HomeAssistant) -> None:
|
|
"""Test options flow."""
|
|
|
|
entry_data = {
|
|
"port": "/dev/ttyUSB0",
|
|
"dsmr_version": "2.2",
|
|
}
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data=entry_data,
|
|
unique_id="/dev/ttyUSB0",
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "init"
|
|
|
|
result = await hass.config_entries.options.async_configure(
|
|
result["flow_id"],
|
|
user_input={
|
|
"time_between_update": 15,
|
|
},
|
|
)
|
|
|
|
with (
|
|
patch("homeassistant.components.dsmr.async_setup_entry", return_value=True),
|
|
patch("homeassistant.components.dsmr.async_unload_entry", return_value=True),
|
|
):
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
assert entry.options == {"time_between_update": 15}
|