Add support for Shelly RPC devices custom TCP port (#110860)

* First coding

* add port to config_entry + gen1 not supported msg

* fix async_step_credentials

* strings

* fix reauth

* fix visit device link

* increased MINOR_VERSION

* apply review comments

* align to latest aioshelly

* missing tests

* introduce port parameter

* update tests

* remove leftover

* remove "port" data_description key

* missing key

* apply review comments

* apply more review comments

* Add tests

* apply review comment

* apply review comment (part 2)

* description update

* fine tuning description

* fix test patching

---------

Co-authored-by: Shay Levy <levyshay1@gmail.com>
This commit is contained in:
Simone Chemelli 2024-03-21 19:58:56 +01:00 committed by GitHub
parent 8141a246b0
commit 8728057b1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 156 additions and 32 deletions

View File

@ -56,6 +56,7 @@ from .utils import (
get_block_device_sleep_period, get_block_device_sleep_period,
get_coap_context, get_coap_context,
get_device_entry_gen, get_device_entry_gen,
get_http_port,
get_rpc_device_wakeup_period, get_rpc_device_wakeup_period,
get_ws_context, get_ws_context,
) )
@ -249,6 +250,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo
entry.data.get(CONF_USERNAME), entry.data.get(CONF_USERNAME),
entry.data.get(CONF_PASSWORD), entry.data.get(CONF_PASSWORD),
device_mac=entry.unique_id, device_mac=entry.unique_id,
port=get_http_port(entry.data),
) )
ws_context = await get_ws_context(hass) ws_context = await get_ws_context(hass)

View File

@ -7,8 +7,9 @@ from typing import Any, Final
from aioshelly.block_device import BlockDevice from aioshelly.block_device import BlockDevice
from aioshelly.common import ConnectionOptions, get_info from aioshelly.common import ConnectionOptions, get_info
from aioshelly.const import BLOCK_GENERATIONS, RPC_GENERATIONS from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATIONS
from aioshelly.exceptions import ( from aioshelly.exceptions import (
CustomPortNotSupported,
DeviceConnectionError, DeviceConnectionError,
FirmwareUnsupported, FirmwareUnsupported,
InvalidAuthError, InvalidAuthError,
@ -23,7 +24,7 @@ from homeassistant.config_entries import (
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlow,
) )
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
@ -42,6 +43,7 @@ from .utils import (
get_block_device_sleep_period, get_block_device_sleep_period,
get_coap_context, get_coap_context,
get_device_entry_gen, get_device_entry_gen,
get_http_port,
get_info_auth, get_info_auth,
get_info_gen, get_info_gen,
get_model_name, get_model_name,
@ -50,7 +52,12 @@ from .utils import (
mac_address_from_name, mac_address_from_name,
) )
HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) CONFIG_SCHEMA: Final = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_HTTP_PORT): vol.Coerce(int),
}
)
BLE_SCANNER_OPTIONS = [ BLE_SCANNER_OPTIONS = [
@ -65,14 +72,20 @@ INTERNAL_WIFI_AP_IP = "192.168.33.1"
async def validate_input( async def validate_input(
hass: HomeAssistant, hass: HomeAssistant,
host: str, host: str,
port: int,
info: dict[str, Any], info: dict[str, Any],
data: dict[str, Any], data: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from HOST_SCHEMA with values provided by the user. Data has the keys from CONFIG_SCHEMA with values provided by the user.
""" """
options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) options = ConnectionOptions(
ip_address=host,
username=data.get(CONF_USERNAME),
password=data.get(CONF_PASSWORD),
port=port,
)
gen = get_info_gen(info) gen = get_info_gen(info)
@ -114,8 +127,10 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Shelly.""" """Handle a config flow for Shelly."""
VERSION = 1 VERSION = 1
MINOR_VERSION = 2
host: str = "" host: str = ""
port: int = DEFAULT_HTTP_PORT
info: dict[str, Any] = {} info: dict[str, Any] = {}
device_info: dict[str, Any] = {} device_info: dict[str, Any] = {}
entry: ConfigEntry | None = None entry: ConfigEntry | None = None
@ -126,9 +141,10 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step.""" """Handle the initial step."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
host: str = user_input[CONF_HOST] host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
try: try:
self.info = await self._async_get_info(host) self.info = await self._async_get_info(host, port)
except DeviceConnectionError: except DeviceConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except FirmwareUnsupported: except FirmwareUnsupported:
@ -140,15 +156,18 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(self.info["mac"]) await self.async_set_unique_id(self.info["mac"])
self._abort_if_unique_id_configured({CONF_HOST: host}) self._abort_if_unique_id_configured({CONF_HOST: host})
self.host = host self.host = host
self.port = port
if get_info_auth(self.info): if get_info_auth(self.info):
return await self.async_step_credentials() return await self.async_step_credentials()
try: try:
device_info = await validate_input( device_info = await validate_input(
self.hass, self.host, self.info, {} self.hass, host, port, self.info, {}
) )
except DeviceConnectionError: except DeviceConnectionError:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except CustomPortNotSupported:
errors["base"] = "custom_port_not_supported"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
LOGGER.exception("Unexpected exception") LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
@ -157,7 +176,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_create_entry( return self.async_create_entry(
title=device_info["title"], title=device_info["title"],
data={ data={
**user_input, CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD],
"model": device_info["model"], "model": device_info["model"],
CONF_GEN: device_info[CONF_GEN], CONF_GEN: device_info[CONF_GEN],
@ -166,7 +186,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "firmware_not_fully_provisioned" errors["base"] = "firmware_not_fully_provisioned"
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=HOST_SCHEMA, errors=errors step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
) )
async def async_step_credentials( async def async_step_credentials(
@ -179,7 +199,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
user_input[CONF_USERNAME] = "admin" user_input[CONF_USERNAME] = "admin"
try: try:
device_info = await validate_input( device_info = await validate_input(
self.hass, self.host, self.info, user_input self.hass, self.host, self.port, self.info, user_input
) )
except InvalidAuthError: except InvalidAuthError:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
@ -195,6 +215,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
data={ data={
**user_input, **user_input,
CONF_HOST: self.host, CONF_HOST: self.host,
CONF_PORT: self.port,
CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD],
"model": device_info["model"], "model": device_info["model"],
CONF_GEN: device_info[CONF_GEN], CONF_GEN: device_info[CONF_GEN],
@ -254,7 +275,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
await self._async_discovered_mac(mac, host) await self._async_discovered_mac(mac, host)
try: try:
self.info = await self._async_get_info(host) # Devices behind range extender doesn't generate zeroconf packets
# so port is always the default one
self.info = await self._async_get_info(host, DEFAULT_HTTP_PORT)
except DeviceConnectionError: except DeviceConnectionError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except FirmwareUnsupported: except FirmwareUnsupported:
@ -277,7 +300,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_credentials() return await self.async_step_credentials()
try: try:
self.device_info = await validate_input(self.hass, self.host, self.info, {}) self.device_info = await validate_input(
self.hass, self.host, self.port, self.info, {}
)
except DeviceConnectionError: except DeviceConnectionError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
@ -329,17 +354,18 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
assert self.entry is not None assert self.entry is not None
host = self.entry.data[CONF_HOST] host = self.entry.data[CONF_HOST]
port = get_http_port(self.entry.data)
if user_input is not None: if user_input is not None:
try: try:
info = await self._async_get_info(host) info = await self._async_get_info(host, port)
except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported):
return self.async_abort(reason="reauth_unsuccessful") return self.async_abort(reason="reauth_unsuccessful")
if get_device_entry_gen(self.entry) != 1: if get_device_entry_gen(self.entry) != 1:
user_input[CONF_USERNAME] = "admin" user_input[CONF_USERNAME] = "admin"
try: try:
await validate_input(self.hass, host, info, user_input) await validate_input(self.hass, host, port, info, user_input)
except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported):
return self.async_abort(reason="reauth_unsuccessful") return self.async_abort(reason="reauth_unsuccessful")
@ -361,9 +387,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def _async_get_info(self, host: str) -> dict[str, Any]: async def _async_get_info(self, host: str, port: int) -> dict[str, Any]:
"""Get info from shelly device.""" """Get info from shelly device."""
return await get_info(async_get_clientsession(self.hass), host) return await get_info(async_get_clientsession(self.hass), host, port=port)
@staticmethod @staticmethod
@callback @callback

View File

@ -59,6 +59,7 @@ from .const import (
) )
from .utils import ( from .utils import (
get_device_entry_gen, get_device_entry_gen,
get_http_port,
get_rpc_device_wakeup_period, get_rpc_device_wakeup_period,
update_device_fw_info, update_device_fw_info,
) )
@ -140,7 +141,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]):
model=MODEL_NAMES.get(self.model, self.model), model=MODEL_NAMES.get(self.model, self.model),
sw_version=self.sw_version, sw_version=self.sw_version,
hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})", hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})",
configuration_url=f"http://{self.entry.data[CONF_HOST]}", configuration_url=f"http://{self.entry.data[CONF_HOST]}:{get_http_port(self.entry.data)}",
) )
self.device_id = device_entry.id self.device_id = device_entry.id

View File

@ -5,10 +5,12 @@
"user": { "user": {
"description": "Before setup, battery-powered devices must be woken up, you can now wake the device up using a button on it.", "description": "Before setup, battery-powered devices must be woken up, you can now wake the device up using a button on it.",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]" "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}, },
"data_description": { "data_description": {
"host": "The hostname or IP address of the Shelly device to connect to." "host": "The hostname or IP address of the Shelly device to connect to.",
"port": "The TCP port of the Shelly device to connect to (Gen2+)."
} }
}, },
"credentials": { "credentials": {
@ -31,7 +33,8 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]", "unknown": "[%key:common::config_flow::error::unknown%]",
"firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support" "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support",
"custom_port_not_supported": "Gen1 device does not support custom port."
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from ipaddress import IPv4Address from ipaddress import IPv4Address
from types import MappingProxyType
from typing import Any, cast from typing import Any, cast
from aiohttp.web import Request, WebSocketResponse from aiohttp.web import Request, WebSocketResponse
@ -11,6 +12,7 @@ from aioshelly.block_device import COAP, Block, BlockDevice
from aioshelly.const import ( from aioshelly.const import (
BLOCK_GENERATIONS, BLOCK_GENERATIONS,
DEFAULT_COAP_PORT, DEFAULT_COAP_PORT,
DEFAULT_HTTP_PORT,
MODEL_1L, MODEL_1L,
MODEL_DIMMER, MODEL_DIMMER,
MODEL_DIMMER_2, MODEL_DIMMER_2,
@ -24,7 +26,7 @@ from aioshelly.rpc_device import RpcDevice, WsServer
from homeassistant.components import network from homeassistant.components import network
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir, singleton from homeassistant.helpers import issue_registry as ir, singleton
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
@ -473,3 +475,8 @@ def is_rpc_wifi_stations_disabled(
return False return False
return True return True
def get_http_port(data: MappingProxyType[str, Any]) -> int:
"""Get port from config entry data."""
return cast(int, data.get(CONF_PORT, DEFAULT_HTTP_PORT))

View File

@ -6,8 +6,9 @@ from ipaddress import ip_address
from typing import Any from typing import Any
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aioshelly.const import MODEL_1, MODEL_PLUS_2PM from aioshelly.const import DEFAULT_HTTP_PORT, MODEL_1, MODEL_PLUS_2PM
from aioshelly.exceptions import ( from aioshelly.exceptions import (
CustomPortNotSupported,
DeviceConnectionError, DeviceConnectionError,
FirmwareUnsupported, FirmwareUnsupported,
InvalidAuthError, InvalidAuthError,
@ -54,17 +55,18 @@ DISCOVERY_INFO_WITH_MAC = zeroconf.ZeroconfServiceInfo(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("gen", "model"), ("gen", "model", "port"),
[ [
(1, MODEL_1), (1, MODEL_1, DEFAULT_HTTP_PORT),
(2, MODEL_PLUS_2PM), (2, MODEL_PLUS_2PM, DEFAULT_HTTP_PORT),
(3, MODEL_PLUS_2PM), (3, MODEL_PLUS_2PM, 11200),
], ],
) )
async def test_form( async def test_form(
hass: HomeAssistant, hass: HomeAssistant,
gen: int, gen: int,
model: str, model: str,
port: int,
mock_block_device: Mock, mock_block_device: Mock,
mock_rpc_device: Mock, mock_rpc_device: Mock,
) -> None: ) -> None:
@ -72,12 +74,18 @@ async def test_form(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == "form" assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
"homeassistant.components.shelly.config_flow.get_info", "homeassistant.components.shelly.config_flow.get_info",
return_value={"mac": "test-mac", "type": MODEL_1, "auth": False, "gen": gen}, return_value={
"mac": "test-mac",
"type": MODEL_1,
"auth": False,
"gen": gen,
"port": port,
},
), patch( ), patch(
"homeassistant.components.shelly.async_setup", return_value=True "homeassistant.components.shelly.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
@ -86,7 +94,7 @@ async def test_form(
) as mock_setup_entry: ) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"host": "1.1.1.1"}, {"host": "1.1.1.1", "port": port},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -94,6 +102,7 @@ async def test_form(
assert result2["title"] == "Test name" assert result2["title"] == "Test name"
assert result2["data"] == { assert result2["data"] == {
"host": "1.1.1.1", "host": "1.1.1.1",
"port": port,
"model": model, "model": model,
"sleep_period": 0, "sleep_period": 0,
"gen": gen, "gen": gen,
@ -102,6 +111,33 @@ async def test_form(
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_form_gen1_custom_port(
hass: HomeAssistant,
mock_block_device: Mock,
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.shelly.config_flow.get_info",
return_value={"mac": "test-mac", "type": MODEL_1, "gen": 1},
), patch(
"aioshelly.block_device.BlockDevice.create",
side_effect=CustomPortNotSupported,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"host": "1.1.1.1", "port": "1100"},
)
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["errors"]["base"] == "custom_port_not_supported"
@pytest.mark.parametrize( @pytest.mark.parametrize(
("gen", "model", "user_input", "username"), ("gen", "model", "user_input", "username"),
[ [
@ -168,6 +204,7 @@ async def test_form_auth(
assert result3["title"] == "Test name" assert result3["title"] == "Test name"
assert result3["data"] == { assert result3["data"] == {
"host": "1.1.1.1", "host": "1.1.1.1",
"port": DEFAULT_HTTP_PORT,
"model": model, "model": model,
"sleep_period": 0, "sleep_period": 0,
"gen": gen, "gen": gen,
@ -757,6 +794,7 @@ async def test_zeroconf_require_auth(
assert result2["title"] == "Test name" assert result2["title"] == "Test name"
assert result2["data"] == { assert result2["data"] == {
"host": "1.1.1.1", "host": "1.1.1.1",
"port": DEFAULT_HTTP_PORT,
"model": MODEL_1, "model": MODEL_1,
"sleep_period": 0, "sleep_period": 0,
"gen": 1, "gen": 1,
@ -1126,7 +1164,7 @@ async def test_sleeping_device_gen2_with_new_firmware(
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == "form" assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
with patch( with patch(
@ -1144,6 +1182,7 @@ async def test_sleeping_device_gen2_with_new_firmware(
assert result["data"] == { assert result["data"] == {
"host": "1.1.1.1", "host": "1.1.1.1",
"port": DEFAULT_HTTP_PORT,
"model": MODEL_PLUS_2PM, "model": MODEL_PLUS_2PM,
"sleep_period": 666, "sleep_period": 666,
"gen": 2, "gen": 2,

View File

@ -4,6 +4,8 @@ from ipaddress import IPv4Address
from unittest.mock import AsyncMock, Mock, call, patch from unittest.mock import AsyncMock, Mock, call, patch
from aioshelly.block_device import COAP from aioshelly.block_device import COAP
from aioshelly.common import ConnectionOptions
from aioshelly.const import MODEL_PLUS_2PM
from aioshelly.exceptions import ( from aioshelly.exceptions import (
DeviceConnectionError, DeviceConnectionError,
FirmwareUnsupported, FirmwareUnsupported,
@ -16,13 +18,14 @@ from homeassistant.components.shelly.const import (
BLOCK_EXPECTED_SLEEP_PERIOD, BLOCK_EXPECTED_SLEEP_PERIOD,
BLOCK_WRONG_SLEEP_PERIOD, BLOCK_WRONG_SLEEP_PERIOD,
CONF_BLE_SCANNER_MODE, CONF_BLE_SCANNER_MODE,
CONF_GEN,
CONF_SLEEP_PERIOD, CONF_SLEEP_PERIOD,
DOMAIN, DOMAIN,
MODELS_WITH_WRONG_SLEEP_PERIOD, MODELS_WITH_WRONG_SLEEP_PERIOD,
BLEScannerMode, BLEScannerMode,
) )
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC, CONNECTION_NETWORK_MAC,
@ -392,6 +395,49 @@ async def test_entry_missing_gen(hass: HomeAssistant, mock_block_device: Mock) -
assert hass.states.get("switch.test_name_channel_1").state is STATE_ON assert hass.states.get("switch.test_name_channel_1").state is STATE_ON
async def test_entry_missing_port(hass: HomeAssistant) -> None:
"""Test successful Gen2 device init when port is missing in entry data."""
data = {
CONF_HOST: "192.168.1.37",
CONF_SLEEP_PERIOD: 0,
"model": MODEL_PLUS_2PM,
CONF_GEN: 2,
}
entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.shelly.RpcDevice.create", return_value=Mock()
) as rpc_device_mock:
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert rpc_device_mock.call_args[0][2] == ConnectionOptions(
ip_address="192.168.1.37", device_mac="123456789ABC", port=80
)
async def test_rpc_entry_custom_port(hass: HomeAssistant) -> None:
"""Test successful Gen2 device init using custom port."""
data = {
CONF_HOST: "192.168.1.37",
CONF_SLEEP_PERIOD: 0,
"model": MODEL_PLUS_2PM,
CONF_GEN: 2,
CONF_PORT: 8001,
}
entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.shelly.RpcDevice.create", return_value=Mock()
) as rpc_device_mock:
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert rpc_device_mock.call_args[0][2] == ConnectionOptions(
ip_address="192.168.1.37", device_mac="123456789ABC", port=8001
)
@pytest.mark.parametrize(("model"), MODELS_WITH_WRONG_SLEEP_PERIOD) @pytest.mark.parametrize(("model"), MODELS_WITH_WRONG_SLEEP_PERIOD)
async def test_sleeping_block_device_wrong_sleep_period( async def test_sleeping_block_device_wrong_sleep_period(
hass: HomeAssistant, mock_block_device: Mock, model: str hass: HomeAssistant, mock_block_device: Mock, model: str