* Add homekit configuration option to bind to default interface
Homekit can fail to be discoverable because the
zeroconf default is to bind to all interfaces
(InterfaceChoice.All). This does not work
on some systems and (InterfaceChoice.Default) which
binds to 0.0.0.0 is needed for homekit to zeroconf
to function.
A new option is available for homekit
zeroconf_default_interface: true
* Update tests
* Update homeassistant/components/homekit/__init__.py
Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com>
* Update homeassistant/components/homekit/__init__.py
Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com>
* Review items
* has a default
* Revert "has a default"
This reverts commit 24ecf0920f
.
Breaks the tests
Co-authored-by: springstan <46536646+springstan@users.noreply.github.com>
373 lines
12 KiB
Python
373 lines
12 KiB
Python
"""Tests for the HomeKit component."""
|
|
from unittest.mock import ANY, Mock, patch
|
|
|
|
import pytest
|
|
from zeroconf import InterfaceChoice
|
|
|
|
from homeassistant import setup
|
|
from homeassistant.components.homekit import (
|
|
MAX_DEVICES,
|
|
STATUS_READY,
|
|
STATUS_RUNNING,
|
|
STATUS_STOPPED,
|
|
STATUS_WAIT,
|
|
HomeKit,
|
|
generate_aid,
|
|
)
|
|
from homeassistant.components.homekit.accessories import HomeBridge
|
|
from homeassistant.components.homekit.const import (
|
|
BRIDGE_NAME,
|
|
CONF_AUTO_START,
|
|
CONF_SAFE_MODE,
|
|
DEFAULT_PORT,
|
|
DEFAULT_SAFE_MODE,
|
|
DOMAIN,
|
|
HOMEKIT_FILE,
|
|
SERVICE_HOMEKIT_RESET_ACCESSORY,
|
|
SERVICE_HOMEKIT_START,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
CONF_IP_ADDRESS,
|
|
CONF_NAME,
|
|
CONF_PORT,
|
|
EVENT_HOMEASSISTANT_START,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
)
|
|
from homeassistant.core import State
|
|
from homeassistant.helpers.entityfilter import generate_filter
|
|
|
|
from tests.components.homekit.common import patch_debounce
|
|
|
|
IP_ADDRESS = "127.0.0.1"
|
|
PATH_HOMEKIT = "homeassistant.components.homekit"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def debounce_patcher():
|
|
"""Patch debounce method."""
|
|
patcher = patch_debounce()
|
|
yield patcher.start()
|
|
patcher.stop()
|
|
|
|
|
|
def test_generate_aid():
|
|
"""Test generate aid method."""
|
|
aid = generate_aid("demo.entity")
|
|
assert isinstance(aid, int)
|
|
assert aid >= 2 and aid <= 18446744073709551615
|
|
|
|
with patch(f"{PATH_HOMEKIT}.adler32") as mock_adler32:
|
|
mock_adler32.side_effect = [0, 1]
|
|
assert generate_aid("demo.entity") is None
|
|
|
|
|
|
async def test_setup_min(hass):
|
|
"""Test async_setup with min config options."""
|
|
with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit:
|
|
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
|
|
|
mock_homekit.assert_any_call(
|
|
hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, None, None
|
|
)
|
|
assert mock_homekit().setup.called is True
|
|
|
|
# Test auto start enabled
|
|
mock_homekit.reset_mock()
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_homekit().start.assert_called_with(ANY)
|
|
|
|
|
|
async def test_setup_auto_start_disabled(hass):
|
|
"""Test async_setup with auto start disabled and test service calls."""
|
|
config = {
|
|
DOMAIN: {
|
|
CONF_AUTO_START: False,
|
|
CONF_NAME: "Test Name",
|
|
CONF_PORT: 11111,
|
|
CONF_IP_ADDRESS: "172.0.0.0",
|
|
CONF_SAFE_MODE: DEFAULT_SAFE_MODE,
|
|
}
|
|
}
|
|
|
|
with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit:
|
|
mock_homekit.return_value = homekit = Mock()
|
|
assert await setup.async_setup_component(hass, DOMAIN, config)
|
|
|
|
mock_homekit.assert_any_call(
|
|
hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE, None, None
|
|
)
|
|
assert mock_homekit().setup.called is True
|
|
|
|
# Test auto_start disabled
|
|
homekit.reset_mock()
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
|
await hass.async_block_till_done()
|
|
assert homekit.start.called is False
|
|
|
|
# Test start call with driver is ready
|
|
homekit.reset_mock()
|
|
homekit.status = STATUS_READY
|
|
|
|
await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True)
|
|
assert homekit.start.called is True
|
|
|
|
# Test start call with driver started
|
|
homekit.reset_mock()
|
|
homekit.status = STATUS_STOPPED
|
|
|
|
await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True)
|
|
assert homekit.start.called is False
|
|
|
|
|
|
async def test_homekit_setup(hass, hk_driver):
|
|
"""Test setup of bridge and driver."""
|
|
homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, DEFAULT_SAFE_MODE)
|
|
|
|
with patch(
|
|
f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver
|
|
) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip:
|
|
mock_ip.return_value = IP_ADDRESS
|
|
await hass.async_add_executor_job(homekit.setup)
|
|
|
|
path = hass.config.path(HOMEKIT_FILE)
|
|
assert isinstance(homekit.bridge, HomeBridge)
|
|
mock_driver.assert_called_with(
|
|
hass,
|
|
address=IP_ADDRESS,
|
|
port=DEFAULT_PORT,
|
|
persist_file=path,
|
|
advertised_address=None,
|
|
interface_choice=None,
|
|
)
|
|
assert homekit.driver.safe_mode is False
|
|
|
|
# Test if stop listener is setup
|
|
assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1
|
|
|
|
|
|
async def test_homekit_setup_ip_address(hass, hk_driver):
|
|
"""Test setup with given IP address."""
|
|
homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, "172.0.0.0", {}, {}, None)
|
|
|
|
with patch(
|
|
f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver
|
|
) as mock_driver:
|
|
await hass.async_add_executor_job(homekit.setup)
|
|
mock_driver.assert_called_with(
|
|
hass,
|
|
address="172.0.0.0",
|
|
port=DEFAULT_PORT,
|
|
persist_file=ANY,
|
|
advertised_address=None,
|
|
interface_choice=None,
|
|
)
|
|
|
|
|
|
async def test_homekit_setup_advertise_ip(hass, hk_driver):
|
|
"""Test setup with given IP address to advertise."""
|
|
homekit = HomeKit(
|
|
hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100"
|
|
)
|
|
|
|
with patch(
|
|
f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver
|
|
) as mock_driver:
|
|
await hass.async_add_executor_job(homekit.setup)
|
|
mock_driver.assert_called_with(
|
|
hass,
|
|
address="0.0.0.0",
|
|
port=DEFAULT_PORT,
|
|
persist_file=ANY,
|
|
advertised_address="192.168.1.100",
|
|
interface_choice=None,
|
|
)
|
|
|
|
|
|
async def test_homekit_setup_interface_choice(hass, hk_driver):
|
|
"""Test setup with interface choice of Default."""
|
|
homekit = HomeKit(
|
|
hass,
|
|
BRIDGE_NAME,
|
|
DEFAULT_PORT,
|
|
"0.0.0.0",
|
|
{},
|
|
{},
|
|
None,
|
|
None,
|
|
InterfaceChoice.Default,
|
|
)
|
|
|
|
with patch(
|
|
f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver
|
|
) as mock_driver:
|
|
await hass.async_add_executor_job(homekit.setup)
|
|
mock_driver.assert_called_with(
|
|
hass,
|
|
address="0.0.0.0",
|
|
port=DEFAULT_PORT,
|
|
persist_file=ANY,
|
|
advertised_address=None,
|
|
interface_choice=InterfaceChoice.Default,
|
|
)
|
|
|
|
|
|
async def test_homekit_setup_safe_mode(hass, hk_driver):
|
|
"""Test if safe_mode flag is set."""
|
|
homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, True, None)
|
|
|
|
with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver):
|
|
await hass.async_add_executor_job(homekit.setup)
|
|
assert homekit.driver.safe_mode is True
|
|
|
|
|
|
async def test_homekit_add_accessory():
|
|
"""Add accessory if config exists and get_acc returns an accessory."""
|
|
homekit = HomeKit("hass", None, None, None, lambda entity_id: True, {}, None, None)
|
|
homekit.driver = "driver"
|
|
homekit.bridge = mock_bridge = Mock()
|
|
|
|
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
|
|
|
|
mock_get_acc.side_effect = [None, "acc", None]
|
|
homekit.add_bridge_accessory(State("light.demo", "on"))
|
|
mock_get_acc.assert_called_with("hass", "driver", ANY, 363398124, {})
|
|
assert not mock_bridge.add_accessory.called
|
|
|
|
homekit.add_bridge_accessory(State("demo.test", "on"))
|
|
mock_get_acc.assert_called_with("hass", "driver", ANY, 294192020, {})
|
|
assert mock_bridge.add_accessory.called
|
|
|
|
homekit.add_bridge_accessory(State("demo.test_2", "on"))
|
|
mock_get_acc.assert_called_with("hass", "driver", ANY, 429982757, {})
|
|
mock_bridge.add_accessory.assert_called_with("acc")
|
|
|
|
|
|
async def test_homekit_remove_accessory():
|
|
"""Remove accessory from bridge."""
|
|
homekit = HomeKit("hass", None, None, None, lambda entity_id: True, {}, None, None)
|
|
homekit.driver = "driver"
|
|
homekit.bridge = mock_bridge = Mock()
|
|
mock_bridge.accessories = {"light.demo": "acc"}
|
|
|
|
acc = homekit.remove_bridge_accessory("light.demo")
|
|
assert acc == "acc"
|
|
assert len(mock_bridge.accessories) == 0
|
|
|
|
|
|
async def test_homekit_entity_filter(hass):
|
|
"""Test the entity filter."""
|
|
entity_filter = generate_filter(["cover"], ["demo.test"], [], [])
|
|
homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None)
|
|
|
|
with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc:
|
|
mock_get_acc.return_value = None
|
|
|
|
homekit.add_bridge_accessory(State("cover.test", "open"))
|
|
assert mock_get_acc.called is True
|
|
mock_get_acc.reset_mock()
|
|
|
|
homekit.add_bridge_accessory(State("demo.test", "on"))
|
|
assert mock_get_acc.called is True
|
|
mock_get_acc.reset_mock()
|
|
|
|
homekit.add_bridge_accessory(State("light.demo", "light"))
|
|
assert mock_get_acc.called is False
|
|
|
|
|
|
async def test_homekit_start(hass, hk_driver, debounce_patcher):
|
|
"""Test HomeKit start method."""
|
|
pin = b"123-45-678"
|
|
homekit = HomeKit(hass, None, None, None, {}, {"cover.demo": {}}, None, None)
|
|
homekit.bridge = Mock()
|
|
homekit.bridge.accessories = []
|
|
homekit.driver = hk_driver
|
|
|
|
hass.states.async_set("light.demo", "on")
|
|
state = hass.states.async_all()[0]
|
|
|
|
with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch(
|
|
f"{PATH_HOMEKIT}.show_setup_message"
|
|
) as mock_setup_msg, patch(
|
|
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
|
|
) as hk_driver_add_acc, patch(
|
|
"pyhap.accessory_driver.AccessoryDriver.start"
|
|
) as hk_driver_start:
|
|
await hass.async_add_executor_job(homekit.start)
|
|
|
|
mock_add_acc.assert_called_with(state)
|
|
mock_setup_msg.assert_called_with(hass, pin)
|
|
hk_driver_add_acc.assert_called_with(homekit.bridge)
|
|
assert hk_driver_start.called
|
|
assert homekit.status == STATUS_RUNNING
|
|
|
|
# Test start() if already started
|
|
hk_driver_start.reset_mock()
|
|
await hass.async_add_executor_job(homekit.start)
|
|
assert not hk_driver_start.called
|
|
|
|
|
|
async def test_homekit_stop(hass):
|
|
"""Test HomeKit stop method."""
|
|
homekit = HomeKit(hass, None, None, None, None, None, None)
|
|
homekit.driver = Mock()
|
|
|
|
assert homekit.status == STATUS_READY
|
|
await hass.async_add_executor_job(homekit.stop)
|
|
homekit.status = STATUS_WAIT
|
|
await hass.async_add_executor_job(homekit.stop)
|
|
homekit.status = STATUS_STOPPED
|
|
await hass.async_add_executor_job(homekit.stop)
|
|
assert homekit.driver.stop.called is False
|
|
|
|
# Test if driver is started
|
|
homekit.status = STATUS_RUNNING
|
|
await hass.async_add_executor_job(homekit.stop)
|
|
assert homekit.driver.stop.called is True
|
|
|
|
|
|
async def test_homekit_reset_accessories(hass):
|
|
"""Test adding too many accessories to HomeKit."""
|
|
entity_id = "light.demo"
|
|
homekit = HomeKit(hass, None, None, None, {}, {entity_id: {}}, None)
|
|
homekit.bridge = Mock()
|
|
|
|
with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch(
|
|
f"{PATH_HOMEKIT}.HomeKit.setup"
|
|
), patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, patch(
|
|
"pyhap.accessory_driver.AccessoryDriver.config_changed"
|
|
) as hk_driver_config_changed:
|
|
|
|
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
|
|
|
aid = generate_aid(entity_id)
|
|
homekit.bridge.accessories = {aid: "acc"}
|
|
homekit.status = STATUS_RUNNING
|
|
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_HOMEKIT_RESET_ACCESSORY,
|
|
{ATTR_ENTITY_ID: entity_id},
|
|
blocking=True,
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert 2 == hk_driver_config_changed.call_count
|
|
assert mock_add_accessory.called
|
|
homekit.status = STATUS_READY
|
|
|
|
|
|
async def test_homekit_too_many_accessories(hass, hk_driver):
|
|
"""Test adding too many accessories to HomeKit."""
|
|
homekit = HomeKit(hass, None, None, None, None, None, None)
|
|
homekit.bridge = Mock()
|
|
homekit.bridge.accessories = range(MAX_DEVICES + 1)
|
|
homekit.driver = hk_driver
|
|
|
|
with patch("pyhap.accessory_driver.AccessoryDriver.start"), patch(
|
|
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
|
|
), patch("homeassistant.components.homekit._LOGGER.warning") as mock_warn:
|
|
await hass.async_add_executor_job(homekit.start)
|
|
assert mock_warn.called is True
|