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
63 changes: 44 additions & 19 deletions homeassistant/components/teltonika/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo

Expand Down Expand Up @@ -59,6 +60,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
try:
device_info = await client.get_device_info()
auth_valid = await client.validate_credentials()
device_id = device_info.device_identifier
if auth_valid and device_id is None:
system_info = await client.get_system_info()
device_id = system_info.mnf_info.serial
except TeltonikaConnectionError as err:
_LOGGER.debug(
"Failed to connect to Teltonika device at %s: %s", base_url, err
Expand All @@ -76,7 +81,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,

return {
"title": device_info.device_name,
"device_id": device_info.device_identifier,
"device_id": device_id,
"host": base_url,
}
Comment thread
karlbeecken marked this conversation as resolved.

Expand Down Expand Up @@ -220,8 +225,29 @@ async def async_step_dhcp(
# No URL variant worked, device not reachable, don't autodiscover
return self.async_abort(reason="cannot_connect")

# Set unique ID and check for existing conf
await self.async_set_unique_id(device_id)
formatted_mac = dr.format_mac(discovery_info.macaddress)

if device_id is None:
# FW with API v1.0 doesn't expose any unique identifier on the
# unauthorized endpoint. Match existing entries by MAC so it
# aborts without asking for credentials again.
device_reg = dr.async_get(self.hass)
if existing := device_reg.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, formatted_mac)}
Comment thread
karlbeecken marked this conversation as resolved.
):
for entry_id in existing.config_entries:
entry = self.hass.config_entries.async_get_entry(entry_id)
if (
entry is not None
and entry.domain == DOMAIN
and entry.unique_id is not None
):
device_id = entry.unique_id
break

# Use the MAC as a placeholder unique_id when nothing matched, so
# parallel DHCP advertisements don't both reach dhcp_confirm.
await self.async_set_unique_id(device_id or formatted_mac)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})

# Store discovery info for the user step
Expand All @@ -243,21 +269,27 @@ async def async_step_dhcp_confirm(
# Get the host from the discovery
host = getattr(self, "_discovered_host", "")

data = {
CONF_HOST: host,
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_VERIFY_SSL: False,
}
try:
# Validate credentials with discovered host
data = {
CONF_HOST: host,
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_VERIFY_SSL: False,
}
info = await validate_input(self.hass, data)

except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception during DHCP confirm")
errors["base"] = "unknown"
else:
# Update unique ID to device identifier if we didn't get it during discovery
await self.async_set_unique_id(
info["device_id"], raise_on_progress=False
)
self._abort_if_unique_id_configured()
self._abort_if_unique_id_configured(updates={CONF_HOST: info["host"]})

return self.async_create_entry(
title=info["title"],
Expand All @@ -268,13 +300,6 @@ async def async_step_dhcp_confirm(
CONF_VERIFY_SSL: False,
},
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception during DHCP confirm")
errors["base"] = "unknown"

return self.async_show_form(
step_id="dhcp_confirm",
Expand Down
14 changes: 13 additions & 1 deletion homeassistant/components/teltonika/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN
Expand Down Expand Up @@ -73,6 +77,14 @@ async def _async_setup(self) -> None:
# Store device info for use by entities
self.device_info = DeviceInfo(
identifiers={(DOMAIN, system_info_response.mnf_info.serial)},
connections={
(CONNECTION_NETWORK_MAC, format_mac(mac))
for mac in (
system_info_response.mnf_info.mac_eth,
system_info_response.mnf_info.mac,
)
if mac
},
name=system_info_response.static.device_name,
manufacturer="Teltonika",
model=system_info_response.static.model,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/teltonika/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["teltasync==0.2.0"]
"requirements": ["teltasync==0.3.0"]
Comment thread
karlbeecken marked this conversation as resolved.
Comment thread
karlbeecken marked this conversation as resolved.
}
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions tests/components/teltonika/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ def mock_teltasync_client(mock_teltasync: MagicMock) -> MagicMock:
return mock_teltasync.return_value


@pytest.fixture(name="rut240_device_info")
def fixture_rut240_device_info() -> UnauthorizedStatusData:
"""Load the unauthorized payload returned by RUT240 firmware 7.6."""
return UnauthorizedStatusData(
**load_json_object_fixture("device_info_rut240.json", DOMAIN)
)


@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
Expand Down
4 changes: 2 additions & 2 deletions tests/components/teltonika/fixtures/device_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
},
"system_info": {
"mnf_info": {
"mac_eth": "001122334455",
"mac_eth": "209727aabbcc",
"name": "RUTX5000XXXX",
"hw_ver": "0202",
"batch": "0024",
"serial": "1234567890",
"mac": "001122334456",
"mac": "209727aabbcd",
"bl_ver": "3.0"
},
"static": {
Expand Down
5 changes: 5 additions & 0 deletions tests/components/teltonika/fixtures/device_info_rut240.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lang": "en",
"device_name": "RUT240",
"api_version": "1.0"
}
4 changes: 2 additions & 2 deletions tests/components/teltonika/fixtures/system_info.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"mnf_info": {
"mac_eth": "001122334455",
"mac_eth": "209727aabbcc",
"name": "RUTX5000XXXX",
"hw_ver": "0202",
"batch": "0024",
"serial": "1234567890",
"mac": "001122334456",
"mac": "209727aabbcd",
Comment thread
karlbeecken marked this conversation as resolved.
"bl_ver": "3.0"
},
"static": {
Expand Down
8 changes: 8 additions & 0 deletions tests/components/teltonika/snapshots/test_init.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
'config_entries_subentries': <ANY>,
'configuration_url': 'https://192.168.1.1',
'connections': set({
tuple(
'mac',
'20:97:27:aa:bb:cc',
),
tuple(
'mac',
'20:97:27:aa:bb:cd',
),
}),
'disabled_by': None,
'entry_type': None,
Expand Down
Loading
Loading