mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add zeroconf discovery to Peblar Rocksolid EV chargers (#133529)
This commit is contained in:
parent
35601480d2
commit
3fe08a7223
@ -8,6 +8,7 @@ from aiohttp import CookieJar
|
||||
from peblar import Peblar, PeblarAuthenticationError, PeblarConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
@ -25,6 +26,8 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_host: str
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@ -49,7 +52,9 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info.product_serial_number)
|
||||
await self.async_set_unique_id(
|
||||
info.product_serial_number, raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title="Peblar", data=user_input)
|
||||
else:
|
||||
@ -69,3 +74,58 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery of a Peblar device."""
|
||||
if not (sn := discovery_info.properties.get("sn")):
|
||||
return self.async_abort(reason="no_serial_number")
|
||||
|
||||
await self.async_set_unique_id(sn)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
|
||||
|
||||
self._host = discovery_info.host
|
||||
self.context.update({"configuration_url": f"http://{discovery_info.host}"})
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
peblar = Peblar(
|
||||
host=self._host,
|
||||
session=async_create_clientsession(
|
||||
self.hass, cookie_jar=CookieJar(unsafe=True)
|
||||
),
|
||||
)
|
||||
try:
|
||||
await peblar.login(password=user_input[CONF_PASSWORD])
|
||||
except PeblarAuthenticationError:
|
||||
errors[CONF_PASSWORD] = "invalid_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="Peblar",
|
||||
data={
|
||||
CONF_HOST: self._host,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
@ -7,5 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["peblar==0.2.1"]
|
||||
"requirements": ["peblar==0.2.1"],
|
||||
"zeroconf": [{ "type": "_http._tcp.local.", "name": "pblr-*" }]
|
||||
}
|
||||
|
@ -11,6 +11,15 @@
|
||||
"host": "The hostname or IP address of your Peblar charger on your home network.",
|
||||
"password": "The same password as you use to log in to the Peblar device' local web interface."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Set up your Peblar EV charger to integrate with Home Assistant.\n\nTo do so, you will need the password you use to log into the Peblar device' web interface.\n\nHome Assistant will automatically configure your Peblar charger for use with Home Assistant.",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::peblar::config::step::user::data_description::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@ -19,7 +28,8 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_serial_number": "The discovered Peblar device did not provide a serial number."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -558,6 +558,10 @@ ZEROCONF = {
|
||||
"manufacturer": "nettigo",
|
||||
},
|
||||
},
|
||||
{
|
||||
"domain": "peblar",
|
||||
"name": "pblr-*",
|
||||
},
|
||||
{
|
||||
"domain": "powerfox",
|
||||
"name": "powerfox*",
|
||||
|
@ -1,12 +1,14 @@
|
||||
"""Configuration flow tests for the Peblar integration."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from peblar import PeblarAuthenticationError, PeblarConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.peblar.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@ -113,3 +115,207 @@ async def test_user_flow_already_configured(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_peblar")
|
||||
async def test_zeroconf_flow(hass: HomeAssistant) -> None:
|
||||
"""Test the zeroconf happy flow from start to finish."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
port=80,
|
||||
hostname="pblr-0000645.local.",
|
||||
name="mock_name",
|
||||
properties={
|
||||
"sn": "23-45-A4O-MOF",
|
||||
"version": "1.6.1+1+WL-1",
|
||||
},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
progress = hass.config_entries.flow.async_progress()
|
||||
assert len(progress) == 1
|
||||
assert progress[0].get("flow_id") == result["flow_id"]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: "OMGPINEAPPLES"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
config_entry = result["result"]
|
||||
assert config_entry.unique_id == "23-45-A4O-MOF"
|
||||
assert config_entry.data == {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PASSWORD: "OMGPINEAPPLES",
|
||||
}
|
||||
assert not config_entry.options
|
||||
|
||||
|
||||
async def test_zeroconf_flow_abort_no_serial(hass: HomeAssistant) -> None:
|
||||
"""Test the zeroconf aborts when it advertises incompatible data."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
port=80,
|
||||
hostname="pblr-0000645.local.",
|
||||
name="mock_name",
|
||||
properties={},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_serial_number"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error"),
|
||||
[
|
||||
(PeblarConnectionError, {"base": "unknown"}),
|
||||
(PeblarAuthenticationError, {CONF_PASSWORD: "invalid_auth"}),
|
||||
(Exception, {"base": "unknown"}),
|
||||
],
|
||||
)
|
||||
async def test_zeroconf_flow_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_peblar: MagicMock,
|
||||
side_effect: Exception,
|
||||
expected_error: dict[str, str],
|
||||
) -> None:
|
||||
"""Test we show form on a error."""
|
||||
mock_peblar.login.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
port=80,
|
||||
hostname="pblr-0000645.local.",
|
||||
name="mock_name",
|
||||
properties={
|
||||
"sn": "23-45-A4O-MOF",
|
||||
"version": "1.6.1+1+WL-1",
|
||||
},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_PASSWORD: "OMGPUPPIES",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["errors"] == expected_error
|
||||
|
||||
mock_peblar.login.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_PASSWORD: "OMGPUPPIES",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
config_entry = result["result"]
|
||||
assert config_entry.unique_id == "23-45-A4O-MOF"
|
||||
assert config_entry.data == {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PASSWORD: "OMGPUPPIES",
|
||||
}
|
||||
assert not config_entry.options
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_peblar")
|
||||
async def test_zeroconf_flow_not_discovered_again(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the zeroconf doesn't re-discover an existing device."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
port=80,
|
||||
hostname="pblr-0000645.local.",
|
||||
name="mock_name",
|
||||
properties={
|
||||
"sn": "23-45-A4O-MOF",
|
||||
"version": "1.6.1+1+WL-1",
|
||||
},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_peblar")
|
||||
async def test_user_flow_with_zeroconf_in_progress(hass: HomeAssistant) -> None:
|
||||
"""Test the full happy path user flow from start to finish.
|
||||
|
||||
While zeroconf discovery is already in progress.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=zeroconf.ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
port=80,
|
||||
hostname="pblr-0000645.local.",
|
||||
name="mock_name",
|
||||
properties={
|
||||
"sn": "23-45-A4O-MOF",
|
||||
"version": "1.6.1+1+WL-1",
|
||||
},
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
|
||||
progress = hass.config_entries.flow.async_progress()
|
||||
assert len(progress) == 1
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
progress = hass.config_entries.flow.async_progress()
|
||||
assert len(progress) == 2
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PASSWORD: "OMGPUPPIES",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert not hass.config_entries.flow.async_progress()
|
||||
|
Loading…
x
Reference in New Issue
Block a user