Add config flow to Volumio (#38252)

This commit is contained in:
On Freund
2020-07-27 10:19:19 +03:00
committed by GitHub
parent 8b06d1d4bd
commit b226a7183f
15 changed files with 539 additions and 137 deletions

View File

@ -926,6 +926,7 @@ omit =
homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/volkszaehler/sensor.py
homeassistant/components/volumio/__init__.py
homeassistant/components/volumio/media_player.py
homeassistant/components/volvooncall/*
homeassistant/components/w800rf32/*

View File

@ -454,6 +454,7 @@ homeassistant/components/vilfo/* @ManneW
homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf
homeassistant/components/volumio/* @OnFreund
homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff

View File

@ -71,7 +71,6 @@ SERVICE_HANDLERS = {
"bose_soundtouch": ("media_player", "soundtouch"),
"bluesound": ("media_player", "bluesound"),
"kodi": ("media_player", "kodi"),
"volumio": ("media_player", "volumio"),
"lg_smart_device": ("media_player", "lg_soundbar"),
"nanoleaf_aurora": ("light", "nanoleaf"),
}
@ -93,6 +92,7 @@ MIGRATED_SERVICE_HANDLERS = [
"songpal",
SERVICE_WEMO,
SERVICE_XIAOMI_GW,
"volumio",
]
DEFAULT_ENABLED = (

View File

@ -1 +1,59 @@
"""The volumio component."""
"""The Volumio integration."""
import asyncio
from pyvolumio import CannotConnectError, Volumio
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN
PLATFORMS = ["media_player"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Volumio component."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Volumio from a config entry."""
volumio = Volumio(
entry.data[CONF_HOST], entry.data[CONF_PORT], async_get_clientsession(hass)
)
try:
info = await volumio.get_system_version()
except CannotConnectError as error:
raise ConfigEntryNotReady from error
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
DATA_VOLUMIO: volumio,
DATA_INFO: info,
}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,122 @@
"""Config flow for Volumio integration."""
import logging
from typing import Optional
from pyvolumio import CannotConnectError, Volumio
import voluptuous as vol
from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_HOST): str, vol.Required(CONF_PORT, default=3000): int}
)
async def validate_input(hass, host, port):
"""Validate the user input allows us to connect."""
volumio = Volumio(host, port, async_get_clientsession(hass))
try:
return await volumio.get_system_info()
except CannotConnectError as error:
raise CannotConnect from error
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Volumio."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize flow."""
self._host: Optional[str] = None
self._port: Optional[int] = None
self._name: Optional[str] = None
self._uuid: Optional[str] = None
@callback
def _async_get_entry(self):
return self.async_create_entry(
title=self._name,
data={
CONF_NAME: self._name,
CONF_HOST: self._host,
CONF_PORT: self._port,
CONF_ID: self._uuid,
},
)
async def _set_uid_and_abort(self):
await self.async_set_unique_id(self._uuid)
self._abort_if_unique_id_configured(
updates={
CONF_HOST: self._host,
CONF_PORT: self._port,
CONF_NAME: self._name,
}
)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
info = None
try:
self._host = user_input[CONF_HOST]
self._port = user_input[CONF_PORT]
info = await validate_input(self.hass, self._host, self._port)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if info is not None:
self._name = info.get("name", self._host)
self._uuid = info.get("id", None)
if self._uuid is not None:
await self._set_uid_and_abort()
return self._async_get_entry()
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
"""Handle zeroconf discovery."""
self._host = discovery_info["host"]
self._port = int(discovery_info["port"])
self._name = discovery_info["properties"]["volumioName"]
self._uuid = discovery_info["properties"]["UUID"]
await self._set_uid_and_abort()
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(self, user_input=None):
"""Handle user-confirmation of discovered node."""
if user_input is not None:
try:
await validate_input(self.hass, self._host, self._port)
return self._async_get_entry()
except CannotConnect:
return self.async_abort(reason="cannot_connect")
return self.async_show_form(
step_id="discovery_confirm", description_placeholders={"name": self._name}
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -0,0 +1,6 @@
"""Constants for the Volumio integration."""
DOMAIN = "volumio"
DATA_INFO = "info"
DATA_VOLUMIO = "volumio"

View File

@ -2,5 +2,8 @@
"domain": "volumio",
"name": "Volumio",
"documentation": "https://www.home-assistant.io/integrations/volumio",
"codeowners": []
}
"codeowners": ["@OnFreund"],
"config_flow": true,
"zeroconf": ["_Volumio._tcp.local."],
"requirements": ["pyvolumio==0.1"]
}

View File

@ -3,15 +3,10 @@ Volumio Platform.
Volumio rest API: https://volumio.github.io/docs/API/REST_API.html
"""
import asyncio
from datetime import timedelta
import logging
import socket
import aiohttp
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
SUPPORT_CLEAR_PLAYLIST,
@ -28,29 +23,19 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_STEP,
)
from homeassistant.const import (
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PORT,
HTTP_OK,
STATE_IDLE,
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = "localhost"
DEFAULT_NAME = "Volumio"
DEFAULT_PORT = 3000
DATA_VOLUMIO = "volumio"
TIMEOUT = 10
SUPPORT_VOLUMIO = (
SUPPORT_PAUSE
| SUPPORT_VOLUME_SET
@ -68,91 +53,59 @@ SUPPORT_VOLUMIO = (
PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=15)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Volumio media player platform."""
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Volumio platform."""
if DATA_VOLUMIO not in hass.data:
hass.data[DATA_VOLUMIO] = {}
data = hass.data[DOMAIN][config_entry.entry_id]
volumio = data[DATA_VOLUMIO]
info = data[DATA_INFO]
uid = config_entry.data[CONF_ID]
name = config_entry.data[CONF_NAME]
# This is a manual configuration?
if discovery_info is None:
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
else:
name = "{} ({})".format(DEFAULT_NAME, discovery_info.get("hostname"))
host = discovery_info.get("host")
port = discovery_info.get("port")
# Only add a device once, so discovered devices do not override manual
# config.
ip_addr = socket.gethostbyname(host)
if ip_addr in hass.data[DATA_VOLUMIO]:
return
entity = Volumio(name, host, port, hass)
hass.data[DATA_VOLUMIO][ip_addr] = entity
entity = Volumio(hass, volumio, uid, name, info)
async_add_entities([entity])
class Volumio(MediaPlayerEntity):
"""Volumio Player Object."""
def __init__(self, name, host, port, hass):
def __init__(self, hass, volumio, uid, name, info):
"""Initialize the media player."""
self.host = host
self.port = port
self.hass = hass
self._url = "{}:{}".format(host, str(port))
self._hass = hass
self._volumio = volumio
self._uid = uid
self._name = name
self._info = info
self._state = {}
self._lastvol = self._state.get("volume", 0)
self._playlists = []
self._currentplaylist = None
async def send_volumio_msg(self, method, params=None):
"""Send message."""
url = f"http://{self.host}:{self.port}/api/v1/{method}/"
_LOGGER.debug("URL: %s params: %s", url, params)
try:
websession = async_get_clientsession(self.hass)
response = await websession.get(url, params=params)
if response.status == HTTP_OK:
data = await response.json()
else:
_LOGGER.error(
"Query failed, response code: %s Full message: %s",
response.status,
response,
)
return False
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
_LOGGER.error(
"Failed communicating with Volumio '%s': %s", self._name, type(error)
)
return False
return data
async def async_update(self):
"""Update state."""
resp = await self.send_volumio_msg("getState")
self._state = await self._volumio.get_state()
await self._async_update_playlists()
if resp is False:
return
self._state = resp.copy()
@property
def unique_id(self):
"""Return the unique id for the entity."""
return self._uid
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def device_info(self):
"""Return device info for this device."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Volumio",
"sw_version": self._info["systemversion"],
"model": self._info["hardware"],
}
@property
def media_content_type(self):
@ -189,13 +142,7 @@ class Volumio(MediaPlayerEntity):
def media_image_url(self):
"""Image url of current playing media."""
url = self._state.get("albumart", None)
if url is None:
return
if str(url[0:2]).lower() == "ht":
mediaurl = url
else:
mediaurl = f"http://{self.host}:{self.port}{url}"
return mediaurl
return self._volumio.canonic_url(url)
@property
def media_seek_position(self):
@ -220,11 +167,6 @@ class Volumio(MediaPlayerEntity):
"""Boolean if volume is currently muted."""
return self._state.get("mute", None)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
@ -247,79 +189,61 @@ class Volumio(MediaPlayerEntity):
async def async_media_next_track(self):
"""Send media_next command to media player."""
await self.send_volumio_msg("commands", params={"cmd": "next"})
await self._volumio.next()
async def async_media_previous_track(self):
"""Send media_previous command to media player."""
await self.send_volumio_msg("commands", params={"cmd": "prev"})
await self._volumio.previous()
async def async_media_play(self):
"""Send media_play command to media player."""
await self.send_volumio_msg("commands", params={"cmd": "play"})
await self._volumio.play()
async def async_media_pause(self):
"""Send media_pause command to media player."""
if self._state["trackType"] == "webradio":
await self.send_volumio_msg("commands", params={"cmd": "stop"})
await self._volumio.stop()
else:
await self.send_volumio_msg("commands", params={"cmd": "pause"})
await self._volumio.pause()
async def async_media_stop(self):
"""Send media_stop command to media player."""
await self.send_volumio_msg("commands", params={"cmd": "stop"})
await self._volumio.stop()
async def async_set_volume_level(self, volume):
"""Send volume_up command to media player."""
await self.send_volumio_msg(
"commands", params={"cmd": "volume", "volume": int(volume * 100)}
)
await self._volumio.set_volume_level(int(volume * 100))
async def async_volume_up(self):
"""Service to send the Volumio the command for volume up."""
await self.send_volumio_msg(
"commands", params={"cmd": "volume", "volume": "plus"}
)
await self._volumio.volume_up()
async def async_volume_down(self):
"""Service to send the Volumio the command for volume down."""
await self.send_volumio_msg(
"commands", params={"cmd": "volume", "volume": "minus"}
)
await self._volumio.volume_down()
async def async_mute_volume(self, mute):
"""Send mute command to media player."""
mutecmd = "mute" if mute else "unmute"
if mute:
# mute is implemented as 0 volume, do save last volume level
self._lastvol = self._state["volume"]
await self.send_volumio_msg(
"commands", params={"cmd": "volume", "volume": mutecmd}
)
return
await self.send_volumio_msg(
"commands", params={"cmd": "volume", "volume": self._lastvol}
)
await self._volumio.mute()
else:
await self._volumio.unmute()
async def async_set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
await self.send_volumio_msg(
"commands", params={"cmd": "random", "value": str(shuffle).lower()}
)
await self._volumio.set_shuffle(shuffle)
async def async_select_source(self, source):
"""Choose a different available playlist and play it."""
"""Choose an available playlist and play it."""
await self._volumio.play_playlist(source)
self._currentplaylist = source
await self.send_volumio_msg(
"commands", params={"cmd": "playplaylist", "name": source}
)
async def async_clear_playlist(self):
"""Clear players playlist."""
await self._volumio.clear_playlist()
self._currentplaylist = None
await self.send_volumio_msg("commands", params={"cmd": "clearQueue"})
@Throttle(PLAYLIST_UPDATE_INTERVAL)
async def _async_update_playlists(self, **kwargs):
"""Update available Volumio playlists."""
self._playlists = await self.send_volumio_msg("listplaylists")
self._playlists = await self._volumio.get_playlists()

View File

@ -0,0 +1,24 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}
},
"discovery_confirm": {
"description": "Do you want to add Volumio (`{name}`) to Home Assistant?",
"title": "Discovered Volumio"
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "Cannot connect to discovered Volumio"
}
}
}

View File

@ -184,6 +184,7 @@ FLOWS = [
"vesync",
"vilfo",
"vizio",
"volumio",
"wemo",
"wiffi",
"withings",

View File

@ -6,6 +6,9 @@ To update, run python3 -m script.hassfest
# fmt: off
ZEROCONF = {
"_Volumio._tcp.local.": [
"volumio"
],
"_api._udp.local.": [
"guardian"
],

View File

@ -1827,6 +1827,9 @@ pyvizio==0.1.49
# homeassistant.components.velux
pyvlx==0.2.16
# homeassistant.components.volumio
pyvolumio==0.1
# homeassistant.components.html5
pywebpush==1.9.2

View File

@ -826,6 +826,9 @@ pyvesync==1.1.0
# homeassistant.components.vizio
pyvizio==0.1.49
# homeassistant.components.volumio
pyvolumio==0.1
# homeassistant.components.html5
pywebpush==1.9.2

View File

@ -0,0 +1 @@
"""Tests for the Volumio integration."""

View File

@ -0,0 +1,252 @@
"""Test the Volumio config flow."""
from homeassistant import config_entries
from homeassistant.components.volumio.config_flow import CannotConnectError
from homeassistant.components.volumio.const import DOMAIN
from tests.async_mock import patch
from tests.common import MockConfigEntry
TEST_SYSTEM_INFO = {"id": "1111-1111-1111-1111", "name": "TestVolumio"}
TEST_CONNECTION = {
"host": "1.1.1.1",
"port": 3000,
}
TEST_DISCOVERY = {
"host": "1.1.1.1",
"port": 3000,
"properties": {"volumioName": "discovered", "UUID": "2222-2222-2222-2222"},
}
TEST_DISCOVERY_RESULT = {
"host": TEST_DISCOVERY["host"],
"port": TEST_DISCOVERY["port"],
"id": TEST_DISCOVERY["properties"]["UUID"],
"name": TEST_DISCOVERY["properties"]["volumioName"],
}
async def test_form(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value=TEST_SYSTEM_INFO,
), patch(
"homeassistant.components.volumio.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.volumio.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_CONNECTION,
)
assert result2["type"] == "create_entry"
assert result2["title"] == "TestVolumio"
assert result2["data"] == {**TEST_SYSTEM_INFO, **TEST_CONNECTION}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_updates_unique_id(hass):
"""Test a duplicate id aborts and updates existing entry."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_SYSTEM_INFO["id"],
data={
"host": "dummy",
"port": 11,
"name": "dummy",
"id": TEST_SYSTEM_INFO["id"],
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value=TEST_SYSTEM_INFO,
), patch("homeassistant.components.volumio.async_setup", return_value=True), patch(
"homeassistant.components.volumio.async_setup_entry", return_value=True,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_CONNECTION,
)
assert result2["type"] == "abort"
assert result2["reason"] == "already_configured"
assert entry.data == {**TEST_SYSTEM_INFO, **TEST_CONNECTION}
async def test_empty_system_info(hass):
"""Test old volumio versions with empty system info."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value={},
), patch(
"homeassistant.components.volumio.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.volumio.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_CONNECTION,
)
assert result2["type"] == "create_entry"
assert result2["title"] == TEST_CONNECTION["host"]
assert result2["data"] == {
"host": TEST_CONNECTION["host"],
"port": TEST_CONNECTION["port"],
"name": TEST_CONNECTION["host"],
"id": None,
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
side_effect=CannotConnectError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_CONNECTION,
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_exception(hass):
"""Test we handle generic error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], TEST_CONNECTION,
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}
async def test_discovery(hass):
"""Test discovery flow works."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
)
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
return_value=TEST_SYSTEM_INFO,
), patch(
"homeassistant.components.volumio.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.volumio.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={},
)
assert result2["type"] == "create_entry"
assert result2["title"] == TEST_DISCOVERY_RESULT["name"]
assert result2["data"] == TEST_DISCOVERY_RESULT
assert result2["result"]
assert result2["result"].unique_id == TEST_DISCOVERY_RESULT["id"]
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_discovery_cannot_connect(hass):
"""Test discovery aborts if cannot connect."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
)
with patch(
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
side_effect=CannotConnectError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={},
)
assert result2["type"] == "abort"
assert result2["reason"] == "cannot_connect"
async def test_discovery_duplicate_data(hass):
"""Test discovery aborts if same mDNS packet arrives."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
)
assert result["type"] == "form"
assert result["step_id"] == "discovery_confirm"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
)
assert result["type"] == "abort"
assert result["reason"] == "already_in_progress"
async def test_discovery_updates_unique_id(hass):
"""Test a duplicate discovery id aborts and updates existing entry."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_DISCOVERY_RESULT["id"],
data={
"host": "dummy",
"port": 11,
"name": "dummy",
"id": TEST_DISCOVERY_RESULT["id"],
},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert entry.data == TEST_DISCOVERY_RESULT