mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
Add zeroconf discovery to openevse (#160318)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -1170,8 +1170,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/open_router/ @joostlek
|
||||
/homeassistant/components/openerz/ @misialq
|
||||
/tests/components/openerz/ @misialq
|
||||
/homeassistant/components/openevse/ @c00w
|
||||
/tests/components/openevse/ @c00w
|
||||
/homeassistant/components/openevse/ @c00w @firstof9
|
||||
/tests/components/openevse/ @c00w @firstof9
|
||||
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
||||
/tests/components/openexchangerates/ @MartinHjelmare
|
||||
/homeassistant/components/opengarage/ @danielhiversen
|
||||
|
||||
@@ -6,9 +6,10 @@ from openevsehttp.__main__ import OpenEVSE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.helpers.service_info import zeroconf
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_ID, DOMAIN
|
||||
|
||||
|
||||
class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@@ -17,6 +18,10 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set up the instance."""
|
||||
self.discovery_info: dict[str, Any] = {}
|
||||
|
||||
async def check_status(self, host: str) -> bool:
|
||||
"""Check if we can connect to the OpenEVSE charger."""
|
||||
|
||||
@@ -62,3 +67,42 @@ class OpenEVSEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=f"OpenEVSE {data[CONF_HOST]}",
|
||||
data=data,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
self._async_abort_entries_match({CONF_HOST: discovery_info.host})
|
||||
|
||||
await self.async_set_unique_id(discovery_info.properties[CONF_ID])
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
|
||||
|
||||
host = discovery_info.host
|
||||
name = f"OpenEVSE {discovery_info.name.split('.')[0]}"
|
||||
self.discovery_info.update(
|
||||
{
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
}
|
||||
)
|
||||
self.context.update({"title_placeholders": {"name": name}})
|
||||
|
||||
if not await self.check_status(host):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={"name": self.discovery_info[CONF_NAME]},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.discovery_info[CONF_NAME],
|
||||
data={CONF_HOST: self.discovery_info[CONF_HOST]},
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Constants for the OpenEVSE integration."""
|
||||
|
||||
CONF_ID = "id"
|
||||
DOMAIN = "openevse"
|
||||
INTEGRATION_TITLE = "OpenEVSE"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"domain": "openevse",
|
||||
"name": "OpenEVSE",
|
||||
"codeowners": ["@c00w"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@c00w", "@firstof9"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/openevse",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openevsehttp"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["python-openevse-http==0.2.1"]
|
||||
"requirements": ["python-openevse-http==0.2.1"],
|
||||
"zeroconf": ["_openevse._tcp.local."]
|
||||
}
|
||||
|
||||
5
homeassistant/generated/zeroconf.py
generated
5
homeassistant/generated/zeroconf.py
generated
@@ -780,6 +780,11 @@ ZEROCONF = {
|
||||
"domain": "octoprint",
|
||||
},
|
||||
],
|
||||
"_openevse._tcp.local.": [
|
||||
{
|
||||
"domain": "openevse",
|
||||
},
|
||||
],
|
||||
"_owserver._tcp.local.": [
|
||||
{
|
||||
"domain": "onewire",
|
||||
|
||||
@@ -47,8 +47,25 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
def has_serial_number() -> bool:
|
||||
"""Return a serial number."""
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def serial_number(has_serial_number: bool) -> str | None:
|
||||
"""Return a serial number."""
|
||||
if has_serial_number:
|
||||
return "deadbeeffeed"
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(serial_number: str) -> MockConfigEntry:
|
||||
"""Create a mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "192.168.1.100"}, entry_id="FAKE"
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "192.168.1.100"},
|
||||
entry_id="FAKE",
|
||||
unique_id=serial_number,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
"""Tests for the OpenEVSE sensor platform."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from homeassistant.components.openevse.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_user_flow(
|
||||
@@ -137,3 +141,128 @@ async def test_import_flow_duplicate(
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_discovery(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_charger: MagicMock
|
||||
) -> None:
|
||||
"""Test zeroconf discovery."""
|
||||
discovery_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.123"),
|
||||
ip_addresses=[ip_address("192.168.1.123")],
|
||||
hostname="openevse-deadbeeffeed.local.",
|
||||
name="openevse-deadbeeffeed._openevse._tcp.local.",
|
||||
port=80,
|
||||
properties={"id": "deadbeeffeed", "type": "openevse"},
|
||||
type="_openevse._tcp.local.",
|
||||
)
|
||||
|
||||
# Trigger the zeroconf step
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
# Should present a confirmation form
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
assert result["description_placeholders"] == {
|
||||
"name": "OpenEVSE openevse-deadbeeffeed"
|
||||
}
|
||||
|
||||
# Confirm the discovery
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
# Should create the entry
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "OpenEVSE openevse-deadbeeffeed"
|
||||
assert result["data"] == {CONF_HOST: "192.168.1.123"}
|
||||
assert result["result"].unique_id == "deadbeeffeed"
|
||||
|
||||
|
||||
async def test_zeroconf_already_configured_unique_id(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_charger: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test zeroconf discovery updates info if unique_id is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
discovery_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.124"),
|
||||
ip_addresses=[ip_address("192.168.1.124"), ip_address("2001:db8::1")],
|
||||
hostname="openevse-deadbeeffeed.local.",
|
||||
name="openevse-deadbeeffeed._openevse._tcp.local.",
|
||||
port=80,
|
||||
properties={"id": "deadbeeffeed", "type": "openevse"},
|
||||
type="_openevse._tcp.local.",
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
# Should abort because unique_id matches, but it updates the config entry
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
# Verify the entry IP was updated to the new discovery IP
|
||||
assert mock_config_entry.data["host"] == "192.168.1.124"
|
||||
|
||||
|
||||
async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant, mock_charger: MagicMock
|
||||
) -> None:
|
||||
"""Test zeroconf discovery with connection failure."""
|
||||
mock_charger.test_and_get.side_effect = TimeoutError
|
||||
discovery_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.123"),
|
||||
ip_addresses=[ip_address("192.168.1.123"), ip_address("2001:db8::1")],
|
||||
hostname="openevse-deadbeeffeed.local.",
|
||||
name="openevse-deadbeeffeed._openevse._tcp.local.",
|
||||
port=80,
|
||||
properties={"id": "deadbeeffeed", "type": "openevse"},
|
||||
type="_openevse._tcp.local.",
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_zeroconf_already_configured_host(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test zeroconf discovery aborts if host is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
discovery_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.1.100"),
|
||||
ip_addresses=[ip_address("192.168.1.100"), ip_address("2001:db8::1")],
|
||||
hostname="openevse-deadbeeffeed.local.",
|
||||
name="openevse-deadbeeffeed._openevse._tcp.local.",
|
||||
port=80,
|
||||
properties={"id": "deadbeeffeed", "type": "openevse"},
|
||||
type="_openevse._tcp.local.",
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
# Should abort because the host matches an existing entry
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
Reference in New Issue
Block a user