Add SenseME integration (#62909)

Co-authored-by: Big Mike <mikelawrence@users.noreply.github.com>
This commit is contained in:
J. Nick Koston 2022-01-07 22:53:05 -10:00 committed by GitHub
parent 509ddc84a5
commit 943aaaeb3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 929 additions and 0 deletions

View File

@ -949,6 +949,10 @@ omit =
homeassistant/components/sense/sensor.py
homeassistant/components/sensehat/light.py
homeassistant/components/sensehat/sensor.py
homeassistant/components/senseme/__init__.py
homeassistant/components/senseme/discovery.py
homeassistant/components/senseme/entity.py
homeassistant/components/senseme/fan.py
homeassistant/components/sensibo/__init__.py
homeassistant/components/sensibo/climate.py
homeassistant/components/serial/sensor.py

View File

@ -123,6 +123,7 @@ homeassistant.components.samsungtv.*
homeassistant.components.scene.*
homeassistant.components.select.*
homeassistant.components.sensor.*
homeassistant.components.senseme.*
homeassistant.components.shelly.*
homeassistant.components.simplisafe.*
homeassistant.components.slack.*

View File

@ -801,6 +801,8 @@ homeassistant/components/select/* @home-assistant/core
tests/components/select/* @home-assistant/core
homeassistant/components/sense/* @kbickar
tests/components/sense/* @kbickar
homeassistant/components/senseme/* @mikelawrence @bdraco
tests/components/senseme/* @mikelawrence @bdraco
homeassistant/components/sensibo/* @andrey-git @gjohansson-ST
tests/components/sensibo/* @andrey-git @gjohansson-ST
homeassistant/components/sentry/* @dcramer @frenck

View File

@ -0,0 +1,36 @@
"""The SenseME integration."""
from __future__ import annotations
from aiosenseme import async_get_device_by_device_info
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import CONF_INFO, DOMAIN, PLATFORMS, UPDATE_RATE
from .discovery import async_start_discovery
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SenseME from a config entry."""
async_start_discovery(hass)
status, device = await async_get_device_by_device_info(
info=entry.data[CONF_INFO], start_first=True, refresh_minutes=UPDATE_RATE
)
if not status:
device.stop()
raise ConfigEntryNotReady(f"Connect to address {device.address} failed")
await device.async_update(not status)
hass.data[DOMAIN][entry.entry_id] = device
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hass.data[DOMAIN][entry.entry_id].stop()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,135 @@
"""Config flow for SenseME."""
from __future__ import annotations
import ipaddress
from typing import Any
from aiosenseme import SensemeDevice, async_get_device_by_ip_address
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import CONF_HOST_MANUAL, CONF_INFO, DOMAIN
from .discovery import async_discover, async_get_discovered_device
DISCOVER_TIMEOUT = 5
class SensemeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle SenseME discovery config flow."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the SenseME config flow."""
self._discovered_devices: list[SensemeDevice] | None = None
self._discovered_device: SensemeDevice | None = None
async def async_step_discovery(
self, discovery_info: DiscoveryInfoType
) -> FlowResult:
"""Handle discovery."""
uuid = discovery_info[CONF_ID]
device = async_get_discovered_device(self.hass, discovery_info[CONF_ID])
host = device.address
await self.async_set_unique_id(uuid)
for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_INFO]["address"] == host:
return self.async_abort(reason="already_configured")
if entry.unique_id != uuid:
continue
if entry.data[CONF_INFO]["address"] != host:
self.hass.config_entries.async_update_entry(
entry, data={CONF_INFO: {**entry.data[CONF_INFO], "address": host}}
)
return self.async_abort(reason="already_configured")
self._discovered_device = device
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""
device = self._discovered_device
assert device is not None
if user_input is not None:
return await self._async_entry_for_device(device)
placeholders = {
"name": device.name,
"model": device.model,
"host": device.address,
}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="discovery_confirm", description_placeholders=placeholders
)
async def _async_entry_for_device(self, device: SensemeDevice) -> FlowResult:
"""Create a config entry for a device."""
await self.async_set_unique_id(device.uuid, raise_on_progress=False)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=device.name,
data={CONF_INFO: device.get_device_info},
)
async def async_step_manual(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle manual entry of an ip address."""
errors = {}
if user_input is not None:
host = user_input[CONF_HOST]
try:
ipaddress.ip_address(host)
except ValueError:
errors[CONF_HOST] = "invalid_host"
else:
if device := await async_get_device_by_ip_address(host):
device.stop()
return await self._async_entry_for_device(device)
errors[CONF_HOST] = "cannot_connect"
return self.async_show_form(
step_id="manual",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initialized by the user."""
if self._discovered_devices is None:
self._discovered_devices = await async_discover(self.hass, DISCOVER_TIMEOUT)
current_ids = self._async_current_ids()
device_selection = {
device.uuid: device.name
for device in self._discovered_devices
if device.uuid not in current_ids
}
if not device_selection:
return await self.async_step_manual(user_input=None)
device_selection[None] = CONF_HOST_MANUAL
if user_input is not None:
if user_input[CONF_DEVICE] is None:
return await self.async_step_manual()
for device in self._discovered_devices:
if device.uuid == user_input[CONF_DEVICE]:
return await self._async_entry_for_device(device)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_DEVICE): vol.In(device_selection)}
),
)

View File

@ -0,0 +1,23 @@
"""Constants for the SenseME integration."""
from homeassistant.const import Platform
DOMAIN = "senseme"
# Periodic fan update rate in minutes
UPDATE_RATE = 1
# data storage
CONF_INFO = "info"
CONF_HOST_MANUAL = "IP Address"
DISCOVERY = "discovery"
# Fan Preset Modes
PRESET_MODE_WHOOSH = "Whoosh"
# Fan Directions
SENSEME_DIRECTION_FORWARD = "FWD"
SENSEME_DIRECTION_REVERSE = "REV"
PLATFORMS = [Platform.FAN]

View File

@ -0,0 +1,63 @@
"""The SenseME integration discovery."""
from __future__ import annotations
import asyncio
from aiosenseme import SensemeDevice, SensemeDiscovery
from homeassistant import config_entries
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant, callback
from .const import DISCOVERY, DOMAIN
@callback
def async_start_discovery(hass: HomeAssistant) -> bool:
"""Start discovery if its not already running."""
domain_data = hass.data.setdefault(DOMAIN, {})
if DISCOVERY in domain_data:
return False # already running
discovery = domain_data[DISCOVERY] = SensemeDiscovery(False)
discovery.add_callback(lambda devices: async_trigger_discovery(hass, devices))
discovery.start()
return True # started
@callback
def async_get_discovered_device(hass: HomeAssistant, uuid: str) -> SensemeDevice:
"""Return a discovered device."""
discovery: SensemeDiscovery = hass.data[DOMAIN][DISCOVERY]
devices: list[SensemeDevice] = discovery.devices
for discovered_device in devices:
if discovered_device.uuid == uuid:
return discovered_device
raise RuntimeError("Discovered device unexpectedly disappeared")
async def async_discover(hass: HomeAssistant, timeout: float) -> list[SensemeDevice]:
"""Discover devices or restart it if its already running."""
started = async_start_discovery(hass)
discovery: SensemeDiscovery = hass.data[DOMAIN][DISCOVERY]
if not started: # already running
discovery.stop()
discovery.start()
await asyncio.sleep(timeout)
devices: list[SensemeDevice] = discovery.devices
return devices
@callback
def async_trigger_discovery(
hass: HomeAssistant,
discovered_devices: list[SensemeDevice],
) -> None:
"""Trigger config flows for discovered devices."""
for device in discovered_devices:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DISCOVERY},
data={CONF_ID: device.uuid},
)
)

View File

@ -0,0 +1,54 @@
"""The SenseME integration entities."""
from __future__ import annotations
from aiosenseme import SensemeDevice
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo, Entity
class SensemeEntity(Entity):
"""Base class for senseme entities."""
_attr_should_poll = False
def __init__(self, device: SensemeDevice, name: str) -> None:
"""Initialize the entity."""
self._device = device
self._attr_name = name
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac)},
name=self._device.name,
manufacturer="Big Ass Fans",
model=self._device.model,
sw_version=self._device.fw_version,
suggested_area=self._device.room_name,
)
@property
def extra_state_attributes(self) -> dict:
"""Get the current device state attributes."""
return {
"room_name": self._device.room_name,
"room_type": self._device.room_type,
}
@callback
def _async_update_attrs(self) -> None:
"""Update attrs from device."""
self._attr_available = self._device.available
@callback
def _async_update_from_device(self) -> None:
"""Process an update from the device."""
self._async_update_attrs()
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Add data updated listener after this object has been initialized."""
self._device.add_callback(self._async_update_from_device)
async def async_will_remove_from_hass(self) -> None:
"""Remove data updated listener after this object has been initialized."""
self._device.remove_callback(self._async_update_from_device)

View File

@ -0,0 +1,125 @@
"""Support for Big Ass Fans SenseME fan."""
from __future__ import annotations
import math
from typing import Any
from aiosenseme import SensemeFan
from homeassistant import config_entries
from homeassistant.components.fan import (
DIRECTION_FORWARD,
DIRECTION_REVERSE,
SUPPORT_DIRECTION,
SUPPORT_SET_SPEED,
FanEntity,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage,
)
from .const import (
DOMAIN,
PRESET_MODE_WHOOSH,
SENSEME_DIRECTION_FORWARD,
SENSEME_DIRECTION_REVERSE,
)
from .entity import SensemeEntity
SENSEME_DIRECTION_TO_HASS = {
SENSEME_DIRECTION_FORWARD: DIRECTION_FORWARD,
SENSEME_DIRECTION_REVERSE: DIRECTION_REVERSE,
}
HASS_DIRECTION_TO_SENSEME = {v: k for k, v in SENSEME_DIRECTION_TO_HASS.items()}
async def async_setup_entry(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up SenseME fans."""
device = hass.data[DOMAIN][entry.entry_id]
if device.is_fan:
async_add_entities([HASensemeFan(device)])
class HASensemeFan(SensemeEntity, FanEntity):
"""SenseME ceiling fan component."""
_attr_supported_features = SUPPORT_SET_SPEED | SUPPORT_DIRECTION
_attr_preset_modes = [PRESET_MODE_WHOOSH]
def __init__(self, device: SensemeFan) -> None:
"""Initialize the entity."""
super().__init__(device, device.name)
self._attr_speed_count = self._device.fan_speed_max
self._attr_unique_id = f"{self._device.uuid}-FAN" # for legacy compat
self._async_update_attrs()
@callback
def _async_update_attrs(self) -> None:
"""Update attrs from device."""
self._attr_is_on = self._device.fan_on
self._attr_current_direction = SENSEME_DIRECTION_TO_HASS.get(
self._device.fan_dir, DIRECTION_FORWARD # None also means forward
)
if self._device.fan_speed is not None:
self._attr_percentage = ranged_value_to_percentage(
self._device.fan_speed_limits, self._device.fan_speed
)
else:
self._attr_percentage = None
whoosh = self._device.fan_whoosh_mode
self._attr_preset_mode = whoosh if whoosh else None
super()._async_update_attrs()
@property
def extra_state_attributes(self) -> dict:
"""Get the current device state attributes."""
return {
"auto_comfort": self._device.fan_autocomfort.capitalize(),
"smartmode": self._device.fan_smartmode.capitalize(),
**super().extra_state_attributes,
}
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
self._device.fan_speed = math.ceil(
percentage_to_ranged_value(self._device.fan_speed_limits, percentage)
)
async def async_turn_on(
self,
speed: str | None = None,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the fan on with a percentage or preset mode."""
if preset_mode is not None:
await self.async_set_preset_mode(preset_mode)
elif percentage is None:
self._device.fan_on = True
else:
await self.async_set_percentage(percentage)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
self._device.fan_on = False
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
if preset_mode != PRESET_MODE_WHOOSH:
raise ValueError(f"Invalid preset mode: {preset_mode}")
# Sleep mode must be off for Whoosh to work.
if self._device.sleep_mode:
self._device.sleep_mode = False
self._device.fan_whoosh_mode = True
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
self._device.fan_dir = HASS_DIRECTION_TO_SENSEME[direction]

View File

@ -0,0 +1,13 @@
{
"domain": "senseme",
"name": "SenseME",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/senseme",
"requirements": [
"aiosenseme==0.5.5"
],
"codeowners": [
"@mikelawrence", "@bdraco"
],
"iot_class": "local_push"
}

View File

@ -0,0 +1,29 @@
{
"config": {
"flow_title": "{name} - {model} ({host})",
"step": {
"user": {
"description": "Select a device, or choose 'IP Address' to manually enter an IP Address.",
"data": {
"device": "Device"
}
},
"discovery_confirm": {
"description": "Do you want to setup {name} - {model} ({host})?"
},
"manual": {
"description": "Enter an IP Address.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
}
}

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_host": "Invalid hostname or IP address"
},
"flow_title": "{name} - {model} ({host})",
"step": {
"discovery_confirm": {
"description": "Do you want to setup {name} - {model} ({host})?"
},
"manual": {
"data": {
"host": "Host"
},
"description": "Enter an IP Address."
},
"user": {
"data": {
"device": "Device"
},
"description": "Select a device, or choose 'IP Address' to manually enter an IP Address."
}
}
}
}

View File

@ -269,6 +269,7 @@ FLOWS = [
"samsungtv",
"screenlogic",
"sense",
"senseme",
"sensibo",
"sentry",
"sharkiq",

View File

@ -1364,6 +1364,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.senseme.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.shelly.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -250,6 +250,9 @@ aiorecollect==1.0.8
# homeassistant.components.ridwell
aioridwell==2021.12.2
# homeassistant.components.senseme
aiosenseme==0.5.5
# homeassistant.components.shelly
aioshelly==1.0.7

View File

@ -182,6 +182,9 @@ aiorecollect==1.0.8
# homeassistant.components.ridwell
aioridwell==2021.12.2
# homeassistant.components.senseme
aiosenseme==0.5.5
# homeassistant.components.shelly
aioshelly==1.0.7

View File

@ -0,0 +1,117 @@
"""Tests for the SenseME integration."""
from contextlib import contextmanager
from unittest.mock import AsyncMock, MagicMock, patch
from aiosenseme import SensemeDevice, SensemeDiscovery
from homeassistant.components.senseme import config_flow
MOCK_NAME = "Haiku Fan"
MOCK_UUID = "77a6b7b3-925d-4695-a415-76d76dca4444"
MOCK_ADDRESS = "127.0.0.1"
device = MagicMock(auto_spec=SensemeDevice)
device.async_update = AsyncMock()
device.model = "Haiku Fan"
device.fan_speed_max = 7
device.mac = "aa:bb:cc:dd:ee:ff"
device.fan_dir = "REV"
device.room_name = "Main"
device.room_type = "Main"
device.fw_version = "1"
device.fan_autocomfort = "on"
device.fan_smartmode = "on"
device.fan_whoosh_mode = "on"
device.name = MOCK_NAME
device.uuid = MOCK_UUID
device.address = MOCK_ADDRESS
device.get_device_info = {
"name": MOCK_NAME,
"uuid": MOCK_UUID,
"mac": "20:F8:5E:92:5A:75",
"address": MOCK_ADDRESS,
"base_model": "FAN,HAIKU,HSERIES",
"has_light": False,
"has_sensor": True,
"is_fan": True,
"is_light": False,
}
device_alternate_ip = MagicMock(auto_spec=SensemeDevice)
device_alternate_ip.async_update = AsyncMock()
device_alternate_ip.model = "Haiku Fan"
device_alternate_ip.fan_speed_max = 7
device_alternate_ip.mac = "aa:bb:cc:dd:ee:ff"
device_alternate_ip.fan_dir = "REV"
device_alternate_ip.room_name = "Main"
device_alternate_ip.room_type = "Main"
device_alternate_ip.fw_version = "1"
device_alternate_ip.fan_autocomfort = "on"
device_alternate_ip.fan_smartmode = "on"
device_alternate_ip.fan_whoosh_mode = "on"
device_alternate_ip.name = MOCK_NAME
device_alternate_ip.uuid = MOCK_UUID
device_alternate_ip.address = "127.0.0.8"
device_alternate_ip.get_device_info = {
"name": MOCK_NAME,
"uuid": MOCK_UUID,
"mac": "20:F8:5E:92:5A:75",
"address": "127.0.0.8",
"base_model": "FAN,HAIKU,HSERIES",
"has_light": False,
"has_sensor": True,
"is_fan": True,
"is_light": False,
}
device2 = MagicMock(auto_spec=SensemeDevice)
device2.async_update = AsyncMock()
device2.model = "Haiku Fan"
device2.fan_speed_max = 7
device2.mac = "aa:bb:cc:dd:ee:ff"
device2.fan_dir = "FWD"
device2.room_name = "Main"
device2.room_type = "Main"
device2.fw_version = "1"
device2.fan_autocomfort = "on"
device2.fan_smartmode = "on"
device2.fan_whoosh_mode = "on"
device2.name = "Device 2"
device2.uuid = "uuid2"
device2.address = "127.0.0.2"
device2.get_device_info = {
"name": "Device 2",
"uuid": "uuid2",
"mac": "20:F8:5E:92:5A:76",
"address": "127.0.0.2",
"base_model": "FAN,HAIKU,HSERIES",
"has_light": True,
"has_sensor": True,
"is_fan": True,
"is_light": False,
}
MOCK_DEVICE = device
MOCK_DEVICE_ALTERNATE_IP = device_alternate_ip
MOCK_DEVICE2 = device2
def _patch_discovery(device=None, no_device=None):
"""Patch discovery."""
mock_senseme_discovery = MagicMock(auto_spec=SensemeDiscovery)
if not no_device:
mock_senseme_discovery.devices = [device or MOCK_DEVICE]
@contextmanager
def _patcher():
with patch.object(config_flow, "DISCOVER_TIMEOUT", 0), patch(
"homeassistant.components.senseme.discovery.SensemeDiscovery",
return_value=mock_senseme_discovery,
):
yield
return _patcher()

View File

@ -0,0 +1,280 @@
"""Test the SenseME config flow."""
from unittest.mock import patch
from homeassistant import config_entries
from homeassistant.components.senseme.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_ID
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from . import (
MOCK_ADDRESS,
MOCK_DEVICE,
MOCK_DEVICE2,
MOCK_DEVICE_ALTERNATE_IP,
MOCK_UUID,
_patch_discovery,
)
from tests.common import MockConfigEntry
async def test_form_user(hass: HomeAssistant) -> None:
"""Test we get the form as a user."""
with _patch_discovery(), patch(
"homeassistant.components.senseme.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"device": MOCK_UUID,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Haiku Fan"
assert result2["data"] == {
"info": MOCK_DEVICE.get_device_info,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_manual_entry(hass: HomeAssistant) -> None:
"""Test we get the form as a user with a discovery but user chooses manual."""
with _patch_discovery():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"device": None,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["step_id"] == "manual"
with patch(
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
return_value=MOCK_DEVICE,
), patch(
"homeassistant.components.senseme.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: MOCK_ADDRESS,
},
)
await hass.async_block_till_done()
assert result3["title"] == "Haiku Fan"
assert result3["data"] == {
"info": MOCK_DEVICE.get_device_info,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_no_discovery(hass: HomeAssistant) -> None:
"""Test we get the form as a user with no discovery."""
with _patch_discovery(no_device=True), patch(
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
return_value=MOCK_DEVICE,
), patch(
"homeassistant.components.senseme.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "not a valid address",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["step_id"] == "manual"
assert result2["errors"] == {CONF_HOST: "invalid_host"}
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_HOST: MOCK_ADDRESS,
},
)
await hass.async_block_till_done()
assert result3["title"] == "Haiku Fan"
assert result3["data"] == {
"info": MOCK_DEVICE.get_device_info,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> None:
"""Test we get the form as a user."""
with _patch_discovery():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"device": None,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["step_id"] == "manual"
with patch(
"homeassistant.components.senseme.config_flow.async_get_device_by_ip_address",
return_value=None,
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: MOCK_ADDRESS,
},
)
await hass.async_block_till_done()
assert result3["type"] == RESULT_TYPE_FORM
assert result3["step_id"] == "manual"
assert result3["errors"] == {CONF_HOST: "cannot_connect"}
async def test_discovery(hass: HomeAssistant) -> None:
"""Test we can setup a discovered device."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"info": MOCK_DEVICE2.get_device_info,
},
unique_id=MOCK_DEVICE2.uuid,
)
entry.add_to_hass(hass)
with _patch_discovery(), patch(
"homeassistant.components.senseme.async_get_device_by_device_info",
return_value=(True, MOCK_DEVICE2),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
with _patch_discovery(), patch(
"homeassistant.components.senseme.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DISCOVERY},
data={CONF_ID: MOCK_UUID},
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"device": MOCK_UUID,
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Haiku Fan"
assert result2["data"] == {
"info": MOCK_DEVICE.get_device_info,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_discovery_existing_device_no_ip_change(hass: HomeAssistant) -> None:
"""Test we can setup a discovered device."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"info": MOCK_DEVICE.get_device_info,
},
unique_id=MOCK_DEVICE.uuid,
)
entry.add_to_hass(hass)
with _patch_discovery(), patch(
"homeassistant.components.senseme.async_get_device_by_device_info",
return_value=(True, MOCK_DEVICE),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
with _patch_discovery():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DISCOVERY},
data={CONF_ID: MOCK_UUID},
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_discovery_existing_device_ip_change(hass: HomeAssistant) -> None:
"""Test a config entry ips get updated from discovery."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"info": MOCK_DEVICE.get_device_info,
},
unique_id=MOCK_DEVICE.uuid,
)
entry.add_to_hass(hass)
with _patch_discovery(device=MOCK_DEVICE_ALTERNATE_IP), patch(
"homeassistant.components.senseme.async_get_device_by_device_info",
return_value=(True, MOCK_DEVICE),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DISCOVERY},
data={CONF_ID: MOCK_UUID},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert entry.data["info"]["address"] == "127.0.0.8"