Add Melnor Bluetooth valve watering Integration (#70457)
This commit is contained in:
parent
f43f440739
commit
8d94c8f74a
16 changed files with 620 additions and 0 deletions
|
@ -720,6 +720,10 @@ omit =
|
|||
homeassistant/components/melcloud/const.py
|
||||
homeassistant/components/melcloud/sensor.py
|
||||
homeassistant/components/melcloud/water_heater.py
|
||||
homeassistant/components/melnor/__init__.py
|
||||
homeassistant/components/melnor/const.py
|
||||
homeassistant/components/melnor/models.py
|
||||
homeassistant/components/melnor/switch.py
|
||||
homeassistant/components/message_bird/notify.py
|
||||
homeassistant/components/met/weather.py
|
||||
homeassistant/components/met_eireann/__init__.py
|
||||
|
|
|
@ -653,6 +653,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/melcloud/ @vilppuvuorinen
|
||||
/homeassistant/components/melissa/ @kennedyshead
|
||||
/tests/components/melissa/ @kennedyshead
|
||||
/homeassistant/components/melnor/ @vanstinator
|
||||
/tests/components/melnor/ @vanstinator
|
||||
/homeassistant/components/met/ @danielhiversen @thimic
|
||||
/tests/components/met/ @danielhiversen @thimic
|
||||
/homeassistant/components/met_eireann/ @DylanGore
|
||||
|
|
74
homeassistant/components/melnor/__init__.py
Normal file
74
homeassistant/components/melnor/__init__.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
"""The melnor integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from melnor_bluetooth.device import Device
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import MelnorDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up melnor from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, entry.data[CONF_ADDRESS])
|
||||
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Couldn't find a nearby device for address: {entry.data[CONF_ADDRESS]}"
|
||||
)
|
||||
|
||||
# Create the device and connect immediately so we can pull down
|
||||
# required attributes before building out our entities
|
||||
device = Device(ble_device)
|
||||
await device.connect(retry_attempts=4)
|
||||
|
||||
if not device.is_connected:
|
||||
raise ConfigEntryNotReady(f"Failed to connect to: {device.mac}")
|
||||
|
||||
@callback
|
||||
def _async_update_ble(
|
||||
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
change: bluetooth.BluetoothChange,
|
||||
) -> None:
|
||||
"""Update from a ble callback."""
|
||||
device.update_ble_device(service_info.device)
|
||||
|
||||
bluetooth.async_register_callback(
|
||||
hass,
|
||||
_async_update_ble,
|
||||
BluetoothCallbackMatcher(address=device.mac),
|
||||
bluetooth.BluetoothScanningMode.PASSIVE,
|
||||
)
|
||||
|
||||
coordinator = MelnorDataUpdateCoordinator(hass, device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
device: Device = hass.data[DOMAIN][entry.entry_id].data
|
||||
|
||||
await device.disconnect()
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
116
homeassistant/components/melnor/config_flow.py
Normal file
116
homeassistant/components/melnor/config_flow.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
"""Config flow for melnor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth import async_discovered_service_info
|
||||
from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER_DATA_START, MANUFACTURER_ID
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for melnor."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovered_address: str
|
||||
self._discovered_addresses: list[str] = []
|
||||
|
||||
def _create_entry(self, address: str) -> FlowResult:
|
||||
"""Create an entry for a discovered device."""
|
||||
|
||||
return self.async_create_entry(
|
||||
title=address,
|
||||
data={
|
||||
CONF_ADDRESS: address,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_bluetooth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle user-confirmation of discovered device."""
|
||||
|
||||
if user_input is not None:
|
||||
return self._create_entry(self._discovered_address)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="bluetooth_confirm",
|
||||
description_placeholders={"name": self._discovered_address},
|
||||
)
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by Bluetooth discovery."""
|
||||
|
||||
address = discovery_info.address
|
||||
|
||||
await self.async_set_unique_id(address)
|
||||
self._abort_if_unique_id_configured(updates={CONF_ADDRESS: address})
|
||||
|
||||
self._discovered_address = address
|
||||
|
||||
self.context["title_placeholders"] = {"name": address}
|
||||
return await self.async_step_bluetooth_confirm()
|
||||
|
||||
async def async_step_pick_device(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the step to pick discovered device."""
|
||||
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self._create_entry(address)
|
||||
|
||||
current_addresses = self._async_current_ids()
|
||||
for discovery_info in async_discovered_service_info(
|
||||
self.hass, connectable=True
|
||||
):
|
||||
|
||||
if discovery_info.manufacturer_id == MANUFACTURER_ID and any(
|
||||
manufacturer_data.startswith(MANUFACTURER_DATA_START)
|
||||
for manufacturer_data in discovery_info.manufacturer_data.values()
|
||||
):
|
||||
|
||||
address = discovery_info.address
|
||||
if (
|
||||
address not in current_addresses
|
||||
and address not in self._discovered_addresses
|
||||
):
|
||||
self._discovered_addresses.append(address)
|
||||
|
||||
addresses = {
|
||||
address
|
||||
for address in self._discovered_addresses
|
||||
if address not in current_addresses
|
||||
}
|
||||
|
||||
# Check if there is at least one device
|
||||
if not addresses:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="pick_device",
|
||||
data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(addresses)}),
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
|
||||
return await self.async_step_pick_device()
|
8
homeassistant/components/melnor/const.py
Normal file
8
homeassistant/components/melnor/const.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
"""Constants for the melnor integration."""
|
||||
|
||||
|
||||
DOMAIN = "melnor"
|
||||
DEFAULT_NAME = "Melnor Bluetooth"
|
||||
|
||||
MANUFACTURER_ID = 13
|
||||
MANUFACTURER_DATA_START = bytearray([89])
|
16
homeassistant/components/melnor/manifest.json
Normal file
16
homeassistant/components/melnor/manifest.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"after_dependencies": ["bluetooth"],
|
||||
"bluetooth": [
|
||||
{
|
||||
"manufacturer_data_start": [89],
|
||||
"manufacturer_id": 13
|
||||
}
|
||||
],
|
||||
"codeowners": ["@vanstinator"],
|
||||
"config_flow": true,
|
||||
"domain": "melnor",
|
||||
"documentation": "https://www.home-assistant.io/integrations/melnor",
|
||||
"iot_class": "local_polling",
|
||||
"name": "Melnor Bluetooth",
|
||||
"requirements": ["melnor-bluetooth==0.0.13"]
|
||||
}
|
74
homeassistant/components/melnor/models.py
Normal file
74
homeassistant/components/melnor/models.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
"""Melnor integration models."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from melnor_bluetooth.device import Device
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
"""Melnor data update coordinator."""
|
||||
|
||||
_device: Device
|
||||
|
||||
def __init__(self, hass: HomeAssistant, device: Device) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Melnor Bluetooth",
|
||||
update_interval=timedelta(seconds=5),
|
||||
)
|
||||
self._device = device
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Update the device state."""
|
||||
|
||||
await self._device.fetch_state()
|
||||
return self._device
|
||||
|
||||
|
||||
class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]):
|
||||
"""Base class for melnor entities."""
|
||||
|
||||
_device: Device
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MelnorDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize a melnor base entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._device = coordinator.data
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device.mac)},
|
||||
manufacturer="Melnor",
|
||||
model=self._device.model,
|
||||
name=self._device.name,
|
||||
)
|
||||
self._attr_name = self._device.name
|
||||
self._attr_unique_id = self._device.mac
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._device = self.coordinator.data
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._device.is_connected
|
13
homeassistant/components/melnor/strings.json
Normal file
13
homeassistant/components/melnor/strings.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "There aren't any Melnor Bluetooth devices nearby."
|
||||
},
|
||||
"step": {
|
||||
"bluetooth_confirm": {
|
||||
"description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?",
|
||||
"title": "Discovered Melnor Bluetooth valve"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
75
homeassistant/components/melnor/switch.py
Normal file
75
homeassistant/components/melnor/switch.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
"""Support for Melnor RainCloud sprinkler water timer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from melnor_bluetooth.device import Valve
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import MelnorBluetoothBaseEntity, MelnorDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_devices: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
switches = []
|
||||
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
# This device may not have 4 valves total, but the library will only expose the right number of valves
|
||||
for i in range(1, 5):
|
||||
if coordinator.data[f"zone{i}"] is not None:
|
||||
switches.append(MelnorSwitch(coordinator, i))
|
||||
|
||||
async_add_devices(switches, True)
|
||||
|
||||
|
||||
class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity):
|
||||
"""A switch implementation for a melnor device."""
|
||||
|
||||
_valve_index: int
|
||||
_attr_icon = "mdi:sprinkler"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MelnorDataUpdateCoordinator,
|
||||
valve_index: int,
|
||||
) -> None:
|
||||
"""Initialize a switch for a melnor device."""
|
||||
super().__init__(coordinator)
|
||||
self._valve_index = valve_index
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"switch-{self._attr_unique_id}-zone{self._valve().id}-manual"
|
||||
)
|
||||
|
||||
self._attr_name = f"{self._device.name} Zone {self._valve().id+1}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
return self._valve().is_watering
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
self._valve().is_watering = True
|
||||
await self._device.push_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
self._valve().is_watering = False
|
||||
await self._device.push_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _valve(self) -> Valve:
|
||||
return cast(Valve, self._device[f"zone{self._valve_index}"])
|
13
homeassistant/components/melnor/translations/en.json
Normal file
13
homeassistant/components/melnor/translations/en.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "There aren't any Melnor Bluetooth devices nearby."
|
||||
},
|
||||
"step": {
|
||||
"bluetooth_confirm": {
|
||||
"description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?",
|
||||
"title": "Discovered Melnor Bluetooth valve"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -138,6 +138,13 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
|
|||
"domain": "led_ble",
|
||||
"local_name": "LEDBlue*"
|
||||
},
|
||||
{
|
||||
"domain": "melnor",
|
||||
"manufacturer_data_start": [
|
||||
89
|
||||
],
|
||||
"manufacturer_id": 13
|
||||
},
|
||||
{
|
||||
"domain": "moat",
|
||||
"local_name": "Moat_S*",
|
||||
|
|
|
@ -218,6 +218,7 @@ FLOWS = {
|
|||
"mazda",
|
||||
"meater",
|
||||
"melcloud",
|
||||
"melnor",
|
||||
"met",
|
||||
"met_eireann",
|
||||
"meteo_france",
|
||||
|
|
|
@ -1033,6 +1033,9 @@ mcstatus==6.0.0
|
|||
# homeassistant.components.meater
|
||||
meater-python==0.0.8
|
||||
|
||||
# homeassistant.components.melnor
|
||||
melnor-bluetooth==0.0.13
|
||||
|
||||
# homeassistant.components.message_bird
|
||||
messagebird==1.2.0
|
||||
|
||||
|
|
|
@ -738,6 +738,9 @@ mcstatus==6.0.0
|
|||
# homeassistant.components.meater
|
||||
meater-python==0.0.8
|
||||
|
||||
# homeassistant.components.melnor
|
||||
melnor-bluetooth==0.0.13
|
||||
|
||||
# homeassistant.components.meteo_france
|
||||
meteofrance-api==1.0.2
|
||||
|
||||
|
|
64
tests/components/melnor/__init__.py
Normal file
64
tests/components/melnor/__init__.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
"""Tests for the melnor integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
|
||||
from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
|
||||
|
||||
FAKE_ADDRESS_1 = "FAKE-ADDRESS-1"
|
||||
FAKE_ADDRESS_2 = "FAKE-ADDRESS-2"
|
||||
|
||||
|
||||
FAKE_SERVICE_INFO_1 = BluetoothServiceInfoBleak(
|
||||
name="YM_TIMER%",
|
||||
address=FAKE_ADDRESS_1,
|
||||
rssi=-63,
|
||||
manufacturer_data={
|
||||
13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc"
|
||||
},
|
||||
service_uuids=[],
|
||||
service_data={},
|
||||
source="local",
|
||||
device=BLEDevice(FAKE_ADDRESS_1, None),
|
||||
advertisement=AdvertisementData(local_name=""),
|
||||
time=0,
|
||||
connectable=True,
|
||||
)
|
||||
|
||||
FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak(
|
||||
name="YM_TIMER%",
|
||||
address=FAKE_ADDRESS_2,
|
||||
rssi=-63,
|
||||
manufacturer_data={
|
||||
13: b"Y\x08\x02\x8f\x00\x00\x00\x00\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0\x00\x00\xf0*\x9b\xcf\xbc"
|
||||
},
|
||||
service_uuids=[],
|
||||
service_data={},
|
||||
source="local",
|
||||
device=BLEDevice(FAKE_ADDRESS_2, None),
|
||||
advertisement=AdvertisementData(local_name=""),
|
||||
time=0,
|
||||
connectable=True,
|
||||
)
|
||||
|
||||
|
||||
def patch_async_setup_entry(return_value=True):
|
||||
"""Patch async setup entry to return True."""
|
||||
return patch(
|
||||
"homeassistant.components.melnor.async_setup_entry",
|
||||
return_value=return_value,
|
||||
)
|
||||
|
||||
|
||||
def patch_async_discovered_service_info(
|
||||
return_value: list[BluetoothServiceInfoBleak] = [FAKE_SERVICE_INFO_1],
|
||||
):
|
||||
"""Patch async_discovered_service_info a mocked device info."""
|
||||
return patch(
|
||||
"homeassistant.components.melnor.config_flow.async_discovered_service_info",
|
||||
return_value=return_value,
|
||||
)
|
147
tests/components/melnor/test_config_flow.py
Normal file
147
tests/components/melnor/test_config_flow.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
"""Test the melnor config flow."""
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.melnor.const import DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_MAC
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import (
|
||||
FAKE_ADDRESS_1,
|
||||
FAKE_SERVICE_INFO_1,
|
||||
FAKE_SERVICE_INFO_2,
|
||||
patch_async_discovered_service_info,
|
||||
patch_async_setup_entry,
|
||||
)
|
||||
|
||||
|
||||
async def test_user_step_no_devices(hass):
|
||||
"""Test we handle no devices found."""
|
||||
with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info(
|
||||
[]
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_user_step_discovered_devices(hass):
|
||||
"""Test we properly handle device picking."""
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info():
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "pick_device"
|
||||
|
||||
with pytest.raises(vol.MultipleInvalid):
|
||||
await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_ADDRESS: "wrong_address"}
|
||||
)
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_ADDRESS: FAKE_ADDRESS_1}
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_step_with_existing_device(hass):
|
||||
"""Test we properly handle device picking."""
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info(
|
||||
[FAKE_SERVICE_INFO_1, FAKE_SERVICE_INFO_2]
|
||||
):
|
||||
|
||||
# Create the config flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_BLUETOOTH,
|
||||
"step_id": "bluetooth_confirm",
|
||||
"user_input": {CONF_MAC: FAKE_ADDRESS_1},
|
||||
},
|
||||
data=FAKE_SERVICE_INFO_1,
|
||||
)
|
||||
|
||||
# And create an entry
|
||||
await hass.config_entries.flow.async_configure(result["flow_id"], user_input={})
|
||||
|
||||
mock_setup_entry.reset_mock()
|
||||
|
||||
# Now open the picker and validate the current address isn't valid
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
|
||||
with pytest.raises(vol.MultipleInvalid):
|
||||
await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_ADDRESS: FAKE_ADDRESS_1}
|
||||
)
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_bluetooth_discovered(hass):
|
||||
"""Test we short circuit to config entry creation."""
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry:
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
data=FAKE_SERVICE_INFO_1,
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
assert result["description_placeholders"] == {"name": FAKE_ADDRESS_1}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_bluetooth_confirm(hass):
|
||||
"""Test we short circuit to config entry creation."""
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry:
|
||||
|
||||
# Create the config flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_BLUETOOTH,
|
||||
"step_id": "bluetooth_confirm",
|
||||
"user_input": {CONF_MAC: FAKE_ADDRESS_1},
|
||||
},
|
||||
data=FAKE_SERVICE_INFO_1,
|
||||
)
|
||||
|
||||
# Interact with it like a user would
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == FAKE_ADDRESS_1
|
||||
assert result2["data"] == {CONF_ADDRESS: FAKE_ADDRESS_1}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
Loading…
Add table
Reference in a new issue