Add option to specify Reolink Basic Service Port (#137603)

* Allow changing the baichuan port

* styling

* Add description

* Add tests

* Review feedback

* capital letters

Co-authored-by: Robert Resch <robert@resch.dev>

---------

Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
starkillerOG 2025-03-16 19:51:06 +01:00 committed by GitHub
parent 8a552aef9d
commit b5fa3e74c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 67 additions and 7 deletions

View File

@ -28,7 +28,7 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin
from .host import ReolinkHost
from .services import async_setup_services
@ -100,6 +100,7 @@ async def async_setup_entry(
or host.api.use_https != config_entry.data[CONF_USE_HTTPS]
or host.api.supported(None, "privacy_mode")
!= config_entry.data.get(CONF_SUPPORTS_PRIVACY_MODE)
or host.api.baichuan.port != config_entry.data.get(CONF_BC_PORT)
):
if host.api.port != config_entry.data[CONF_PORT]:
_LOGGER.warning(
@ -108,10 +109,21 @@ async def async_setup_entry(
config_entry.data[CONF_PORT],
host.api.port,
)
if (
config_entry.data.get(CONF_BC_PORT, host.api.baichuan.port)
!= host.api.baichuan.port
):
_LOGGER.warning(
"Baichuan port of Reolink %s, changed from %s to %s",
host.api.nvr_name,
config_entry.data.get(CONF_BC_PORT),
host.api.baichuan.port,
)
data = {
**config_entry.data,
CONF_PORT: host.api.port,
CONF_USE_HTTPS: host.api.use_https,
CONF_BC_PORT: host.api.baichuan.port,
CONF_SUPPORTS_PRIVACY_MODE: host.api.supported(None, "privacy_mode"),
}
hass.config_entries.async_update_entry(config_entry, data=data)

View File

@ -8,6 +8,7 @@ import logging
from typing import Any
from reolink_aio.api import ALLOWED_SPECIAL_CHARS
from reolink_aio.baichuan import DEFAULT_BC_PORT
from reolink_aio.exceptions import (
ApiError,
CredentialsInvalidError,
@ -37,7 +38,7 @@ from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .exceptions import (
PasswordIncompatible,
ReolinkException,
@ -287,6 +288,7 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
if not errors:
user_input[CONF_PORT] = host.api.port
user_input[CONF_USE_HTTPS] = host.api.use_https
user_input[CONF_BC_PORT] = host.api.baichuan.port
user_input[CONF_SUPPORTS_PRIVACY_MODE] = host.api.supported(
None, "privacy_mode"
)
@ -326,8 +328,9 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
if errors:
data_schema = data_schema.extend(
{
vol.Optional(CONF_PORT): cv.positive_int,
vol.Optional(CONF_PORT): cv.port,
vol.Required(CONF_USE_HTTPS, default=False): bool,
vol.Required(CONF_BC_PORT, default=DEFAULT_BC_PORT): cv.port,
}
)

View File

@ -3,4 +3,5 @@
DOMAIN = "reolink"
CONF_USE_HTTPS = "use_https"
CONF_BC_PORT = "baichuan_port"
CONF_SUPPORTS_PRIVACY_MODE = "privacy_mode_supported"

View File

@ -12,6 +12,7 @@ from typing import Any, Literal
import aiohttp
from aiohttp.web import Request
from reolink_aio.api import ALLOWED_SPECIAL_CHARS, Host
from reolink_aio.baichuan import DEFAULT_BC_PORT
from reolink_aio.enums import SubType
from reolink_aio.exceptions import NotSupportedError, ReolinkError, SubscriptionError
@ -33,7 +34,7 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.storage import Store
from homeassistant.util.ssl import SSLCipherList
from .const import CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .exceptions import (
PasswordIncompatible,
ReolinkSetupException,
@ -91,6 +92,7 @@ class ReolinkHost:
protocol=options[CONF_PROTOCOL],
timeout=DEFAULT_TIMEOUT,
aiohttp_get_session_callback=get_aiohttp_session,
bc_port=config.get(CONF_BC_PORT, DEFAULT_BC_PORT),
)
self.last_wake: float = 0

View File

@ -8,13 +8,15 @@
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"use_https": "Enable HTTPS",
"baichuan_port": "Basic service port",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The hostname or IP address of your Reolink device. For example: '192.168.1.25'.",
"port": "The port to connect to the Reolink device. For HTTP normally: '80', for HTTPS normally '443'.",
"port": "The HTTP(s) port to connect to the Reolink device API. For HTTP normally: '80', for HTTPS normally '443'.",
"use_https": "Use a HTTPS (SSL) connection to the Reolink device.",
"baichuan_port": "The 'Basic Service Port' to connect to the Reolink device over TCP. Normally '9000' unless manually changed in the Reolink desktop client.",
"username": "Username to login to the Reolink device itself. Not the Reolink cloud account.",
"password": "Password to login to the Reolink device itself. Not the Reolink cloud account."
}

View File

@ -10,6 +10,7 @@ from reolink_aio.exceptions import ReolinkError
from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL
from homeassistant.components.reolink.const import (
CONF_BC_PORT,
CONF_SUPPORTS_PRIVACY_MODE,
CONF_USE_HTTPS,
DOMAIN,
@ -48,6 +49,7 @@ TEST_ITEM_NUMBER = "P000"
TEST_CAM_MODEL = "RLC-123"
TEST_DUO_MODEL = "Reolink Duo PoE"
TEST_PRIVACY = True
TEST_BC_PORT = 5678
@pytest.fixture
@ -136,6 +138,7 @@ def reolink_connect_class() -> Generator[MagicMock]:
# Baichuan
host_mock.baichuan = create_autospec(Baichuan)
# Disable tcp push by default for tests
host_mock.baichuan.port = TEST_BC_PORT
host_mock.baichuan.events_active = False
host_mock.baichuan.privacy_mode.return_value = False
host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error")
@ -175,6 +178,7 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry:
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,

View File

@ -19,6 +19,7 @@ from homeassistant import config_entries
from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL
from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL
from homeassistant.components.reolink.const import (
CONF_BC_PORT,
CONF_SUPPORTS_PRIVACY_MODE,
CONF_USE_HTTPS,
DOMAIN,
@ -40,6 +41,7 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .conftest import (
DHCP_FORMATTED_MAC,
TEST_BC_PORT,
TEST_HOST,
TEST_HOST2,
TEST_MAC,
@ -88,6 +90,7 @@ async def test_config_flow_manual_success(
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY,
CONF_BC_PORT: TEST_BC_PORT,
}
assert result["options"] == {
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -140,6 +143,7 @@ async def test_config_flow_privacy_success(
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY,
CONF_BC_PORT: TEST_BC_PORT,
}
assert result["options"] == {
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -290,6 +294,7 @@ async def test_config_flow_errors(
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
)
@ -302,6 +307,7 @@ async def test_config_flow_errors(
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY,
CONF_BC_PORT: TEST_BC_PORT,
}
assert result["options"] == {
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -322,6 +328,7 @@ async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) ->
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: "rtsp",
@ -360,6 +367,7 @@ async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None:
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -405,6 +413,7 @@ async def test_reauth_abort_unique_id_mismatch(
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -474,6 +483,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY,
CONF_BC_PORT: TEST_BC_PORT,
}
assert result["options"] == {
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -496,6 +506,7 @@ async def test_dhcp_ip_update_aborted_if_wrong_mac(
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -536,6 +547,7 @@ async def test_dhcp_ip_update_aborted_if_wrong_mac(
protocol=DEFAULT_PROTOCOL,
timeout=DEFAULT_TIMEOUT,
aiohttp_get_session_callback=ANY,
bc_port=TEST_BC_PORT,
)
assert expected_call in reolink_connect_class.call_args_list
@ -593,6 +605,7 @@ async def test_dhcp_ip_update(
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -635,6 +648,7 @@ async def test_dhcp_ip_update(
protocol=DEFAULT_PROTOCOL,
timeout=DEFAULT_TIMEOUT,
aiohttp_get_session_callback=ANY,
bc_port=TEST_BC_PORT,
)
assert expected_call in reolink_connect_class.call_args_list
@ -671,6 +685,7 @@ async def test_dhcp_ip_update_ingnored_if_still_connected(
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -702,6 +717,7 @@ async def test_dhcp_ip_update_ingnored_if_still_connected(
protocol=DEFAULT_PROTOCOL,
timeout=DEFAULT_TIMEOUT,
aiohttp_get_session_callback=ANY,
bc_port=TEST_BC_PORT,
)
assert expected_call in reolink_connect_class.call_args_list
@ -731,6 +747,7 @@ async def test_reconfig(hass: HomeAssistant, mock_setup_entry: MagicMock) -> Non
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,
@ -777,6 +794,7 @@ async def test_reconfig_abort_unique_id_mismatch(
CONF_PASSWORD: TEST_PASSWORD,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,

View File

@ -19,7 +19,7 @@ from homeassistant.components.reolink import (
FIRMWARE_UPDATE_INTERVAL,
NUM_CRED_ERRORS,
)
from homeassistant.components.reolink.const import DOMAIN
from homeassistant.components.reolink.const import CONF_BC_PORT, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_PORT,
@ -38,6 +38,7 @@ from homeassistant.helpers import (
from homeassistant.setup import async_setup_component
from .conftest import (
TEST_BC_PORT,
TEST_CAM_MODEL,
TEST_HOST_MODEL,
TEST_MAC,
@ -762,6 +763,21 @@ async def test_port_changed(
assert config_entry.data[CONF_PORT] == 4567
async def test_baichuan_port_changed(
hass: HomeAssistant,
reolink_connect: MagicMock,
config_entry: MockConfigEntry,
) -> None:
"""Test config_entry baichuan port update when it has changed during initial login."""
assert config_entry.data[CONF_BC_PORT] == TEST_BC_PORT
reolink_connect.baichuan.port = 8901
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.data[CONF_BC_PORT] == 8901
async def test_privacy_mode_on(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,

View File

@ -15,7 +15,7 @@ from homeassistant.components.media_source import (
async_resolve_media,
)
from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL
from homeassistant.components.reolink.const import CONF_USE_HTTPS, DOMAIN
from homeassistant.components.reolink.const import CONF_BC_PORT, CONF_USE_HTTPS, DOMAIN
from homeassistant.components.stream import DOMAIN as MEDIA_STREAM_DOMAIN
from homeassistant.const import (
CONF_HOST,
@ -31,6 +31,7 @@ from homeassistant.helpers.device_registry import format_mac
from homeassistant.setup import async_setup_component
from .conftest import (
TEST_BC_PORT,
TEST_HOST2,
TEST_HOST_MODEL,
TEST_MAC2,
@ -348,6 +349,7 @@ async def test_browsing_not_loaded(
CONF_PASSWORD: TEST_PASSWORD2,
CONF_PORT: TEST_PORT,
CONF_USE_HTTPS: TEST_USE_HTTPS,
CONF_BC_PORT: TEST_BC_PORT,
},
options={
CONF_PROTOCOL: DEFAULT_PROTOCOL,