mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
8141a246b0
commit
8728057b1b
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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%]",
|
||||||
|
@ -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))
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user