Skip to content

Commit

Permalink
components/husqvarna_automower_ble: Support PIN
Browse files Browse the repository at this point in the history
All Automowers are setup with a 4 digit PIN. Depending the the model of
the Automower and the security level set on the device by the user the
PIN is required at boot and when performing certain operations.

The current Home Assistant integration doesn't require a PIN for certain
models with lower security levels. It seems like higher security levels
or certain models always require a PIN though. So the integration
currently doesn't work for all models or configurations.

As such, let's request a PIN from users when setting up the integration
so that we can use that for communicating with the mower. This should
make the integration more robust for a range of different models and
security levels as we can send the PIN as part of the BLE setup.

Fixes: #131321
Signed-off-by: Alistair Francis <alistair@alistair23.me>
  • Loading branch information
alistair23 committed Jan 12, 2025
1 parent bce7e9b commit d7a234e
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 12 deletions.
5 changes: 3 additions & 2 deletions homeassistant/components/husqvarna_automower_ble/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, Platform
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

Expand All @@ -23,9 +23,10 @@
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Husqvarna Autoconnect Bluetooth from a config entry."""
address = entry.data[CONF_ADDRESS]
pin = int(entry.data[CONF_PIN])
channel_id = entry.data[CONF_CLIENT_ID]

mower = Mower(channel_id, address)
mower = Mower(channel_id, address, pin)

await close_stale_connections_by_address(address)

Expand Down
33 changes: 30 additions & 3 deletions homeassistant/components/husqvarna_automower_ble/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN

from .const import DOMAIN, LOGGER

Expand Down Expand Up @@ -60,7 +60,28 @@ async def async_step_bluetooth(
self.address = discovery_info.address
await self.async_set_unique_id(self.address)
self._abort_if_unique_id_configured()
return await self.async_step_confirm()
return await self.async_step_bluetooth_confirm()

async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""

if user_input is not None:
self.pin = user_input[CONF_PIN]

Check warning on line 71 in homeassistant/components/husqvarna_automower_ble/config_flow.py

View workflow job for this annotation

GitHub Actions / Check pylint

W0201: Attribute 'pin' defined outside __init__ (attribute-defined-outside-init)
await self.async_set_unique_id(self.address, raise_on_progress=False)
self._abort_if_unique_id_configured()
return await self.async_step_confirm()

self._set_confirm_only()
return self.async_show_form(
step_id="bluetooth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PIN): str,
},
),
)

async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
Expand Down Expand Up @@ -88,7 +109,11 @@ async def async_step_confirm(
if user_input is not None:
return self.async_create_entry(
title=title,
data={CONF_ADDRESS: self.address, CONF_CLIENT_ID: channel_id},
data={
CONF_ADDRESS: self.address,
CONF_CLIENT_ID: channel_id,
CONF_PIN: self.pin,
},
)

self.context["title_placeholders"] = {
Expand All @@ -107,6 +132,7 @@ async def async_step_user(
"""Handle the initial step."""
if user_input is not None:
self.address = user_input[CONF_ADDRESS]
self.pin = user_input[CONF_PIN]

Check warning on line 135 in homeassistant/components/husqvarna_automower_ble/config_flow.py

View workflow job for this annotation

GitHub Actions / Check pylint

W0201: Attribute 'pin' defined outside __init__ (attribute-defined-outside-init)
await self.async_set_unique_id(self.address, raise_on_progress=False)
self._abort_if_unique_id_configured()
return await self.async_step_confirm()
Expand All @@ -116,6 +142,7 @@ async def async_step_user(
data_schema=vol.Schema(
{
vol.Required(CONF_ADDRESS): str,
vol.Required(CONF_PIN): str,
},
),
)
3 changes: 2 additions & 1 deletion tests/components/husqvarna_automower_ble/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

from homeassistant.components.husqvarna_automower_ble.const import DOMAIN
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN

from . import AUTOMOWER_SERVICE_INFO

Expand Down Expand Up @@ -57,6 +57,7 @@ def mock_config_entry() -> MockConfigEntry:
data={
CONF_ADDRESS: AUTOMOWER_SERVICE_INFO.address,
CONF_CLIENT_ID: 1197489078,
CONF_PIN: 1234,
},
unique_id=AUTOMOWER_SERVICE_INFO.address,
)
33 changes: 27 additions & 6 deletions tests/components/husqvarna_automower_ble/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from homeassistant.components.husqvarna_automower_ble.const import DOMAIN
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType

Expand Down Expand Up @@ -48,7 +48,10 @@ async def test_user_selection(hass: HomeAssistant) -> None:

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000001"},
user_input={
CONF_ADDRESS: "00000000-0000-0000-0000-000000000001",
CONF_PIN: "1234",
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm"
Expand All @@ -64,6 +67,7 @@ async def test_user_selection(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_ADDRESS: "00000000-0000-0000-0000-000000000001",
CONF_CLIENT_ID: 1197489078,
CONF_PIN: "1234",
}


Expand All @@ -73,6 +77,14 @@ async def test_bluetooth(hass: HomeAssistant) -> None:
inject_bluetooth_service_info(hass, AUTOMOWER_SERVICE_INFO)
await hass.async_block_till_done(wait_background_tasks=True)

result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
assert result["step_id"] == "bluetooth_confirm"

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PIN: "1234"},
)

result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
assert result["step_id"] == "confirm"
assert result["context"]["unique_id"] == "00000000-0000-0000-0000-000000000003"
Expand All @@ -88,6 +100,7 @@ async def test_bluetooth(hass: HomeAssistant) -> None:
assert result["data"] == {
CONF_ADDRESS: "00000000-0000-0000-0000-000000000003",
CONF_CLIENT_ID: 1197489078,
CONF_PIN: "1234",
}


Expand Down Expand Up @@ -126,7 +139,10 @@ async def test_failed_connect(

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000001"},
user_input={
CONF_ADDRESS: "00000000-0000-0000-0000-000000000001",
CONF_PIN: "1234",
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm"
Expand All @@ -142,6 +158,7 @@ async def test_failed_connect(
assert result["data"] == {
CONF_ADDRESS: "00000000-0000-0000-0000-000000000001",
CONF_CLIENT_ID: 1197489078,
CONF_PIN: "1234",
}


Expand Down Expand Up @@ -169,7 +186,10 @@ async def test_duplicate_entry(

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_ADDRESS: "00000000-0000-0000-0000-000000000003"},
user_input={
CONF_ADDRESS: "00000000-0000-0000-0000-000000000003",
CONF_PIN: "1234",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
Expand All @@ -188,11 +208,12 @@ async def test_exception_connect(
mock_automower_client.probe_gatts.side_effect = BleakError

result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0]
assert result["step_id"] == "confirm"
assert result["step_id"] == "bluetooth_confirm"

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={},
user_input={CONF_PIN: "1234"},
)

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"

0 comments on commit d7a234e

Please sign in to comment.