Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
8 changes: 8 additions & 0 deletions homeassistant/components/teltonika/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .coordinator import TeltonikaDataUpdateCoordinator
Expand Down Expand Up @@ -51,6 +52,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeltonikaConfigEntry) ->

assert coordinator.device_info is not None

# Register the device explicitly so MAC connections are present even
# when no sensor entities have been created yet.
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id,
**coordinator.device_info,
Comment thread
karlbeecken marked this conversation as resolved.
Outdated
)
Comment thread
karlbeecken marked this conversation as resolved.
Outdated

# Store runtime data
entry.runtime_data = coordinator

Expand Down
64 changes: 44 additions & 20 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,8 @@ 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()
if auth_valid and device_info.device_identifier is None:
system_info = await client.get_system_info()
Comment thread
karlbeecken marked this conversation as resolved.
Outdated
except TeltonikaConnectionError as err:
_LOGGER.debug(
"Failed to connect to Teltonika device at %s: %s", base_url, err
Expand All @@ -76,7 +79,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_info.device_identifier or system_info.mnf_info.serial,
Comment thread
karlbeecken marked this conversation as resolved.
Outdated
Comment thread
karlbeecken marked this conversation as resolved.
Outdated
"host": base_url,
}
Comment thread
karlbeecken marked this conversation as resolved.

Expand Down Expand Up @@ -220,9 +223,31 @@ 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)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
if device_id is not None:
# Set unique ID and check for existing conf
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
else:
# Older firmware (API v1.0) does not expose the serial without
# authentication, so we fall back to the DHCP MAC as the flow
# unique_id (suppresses parallel discovery flows for the same
# device) and look the device up in the registry by MAC.
mac = dr.format_mac(discovery_info.macaddress)
await self.async_set_unique_id(mac)
Comment thread
karlbeecken marked this conversation as resolved.
Outdated
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
existing = dr.async_get(self.hass).async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, mac)}
)
if existing is not None:
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:
return self.async_update_reload_and_abort(
Comment thread
karlbeecken marked this conversation as resolved.
Outdated
entry,
data_updates={CONF_HOST: host},
Comment thread
karlbeecken marked this conversation as resolved.
Outdated
reason="already_configured",
reload_even_if_entry_is_unchanged=False,
)
Comment thread
karlbeecken marked this conversation as resolved.
Outdated

# Store discovery info for the user step
self.context["title_placeholders"] = {
Expand All @@ -243,21 +268,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 +299,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