Any accessories, regardless of transport, that enable physical access to the home, such as door locks, must not be bridged.
307 lines
10 KiB
Python
307 lines
10 KiB
Python
"""Test HomeKit util module."""
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.homekit.const import (
|
|
BRIDGE_NAME,
|
|
CONF_FEATURE,
|
|
CONF_FEATURE_LIST,
|
|
CONF_LINKED_BATTERY_SENSOR,
|
|
CONF_LOW_BATTERY_THRESHOLD,
|
|
DEFAULT_CONFIG_FLOW_PORT,
|
|
DOMAIN,
|
|
FEATURE_ON_OFF,
|
|
FEATURE_PLAY_PAUSE,
|
|
HOMEKIT_PAIRING_QR,
|
|
HOMEKIT_PAIRING_QR_SECRET,
|
|
TYPE_FAUCET,
|
|
TYPE_OUTLET,
|
|
TYPE_SHOWER,
|
|
TYPE_SPRINKLER,
|
|
TYPE_SWITCH,
|
|
TYPE_VALVE,
|
|
)
|
|
from homeassistant.components.homekit.util import (
|
|
accessory_friendly_name,
|
|
async_find_next_available_port,
|
|
cleanup_name_for_homekit,
|
|
convert_to_float,
|
|
density_to_air_quality,
|
|
dismiss_setup_message,
|
|
format_sw_version,
|
|
port_is_available,
|
|
show_setup_message,
|
|
state_needs_accessory_mode,
|
|
temperature_to_homekit,
|
|
temperature_to_states,
|
|
validate_entity_config as vec,
|
|
validate_media_player_features,
|
|
)
|
|
from homeassistant.components.persistent_notification import (
|
|
ATTR_MESSAGE,
|
|
ATTR_NOTIFICATION_ID,
|
|
DOMAIN as PERSISTENT_NOTIFICATION_DOMAIN,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_CODE,
|
|
ATTR_SUPPORTED_FEATURES,
|
|
CONF_NAME,
|
|
CONF_PORT,
|
|
CONF_TYPE,
|
|
STATE_UNKNOWN,
|
|
TEMP_CELSIUS,
|
|
TEMP_FAHRENHEIT,
|
|
)
|
|
from homeassistant.core import State
|
|
|
|
from .util import async_init_integration
|
|
|
|
from tests.common import MockConfigEntry, async_mock_service
|
|
|
|
|
|
def test_validate_entity_config():
|
|
"""Test validate entities."""
|
|
configs = [
|
|
None,
|
|
[],
|
|
"string",
|
|
12345,
|
|
{"invalid_entity_id": {}},
|
|
{"demo.test": 1},
|
|
{"binary_sensor.demo": {CONF_LINKED_BATTERY_SENSOR: None}},
|
|
{"binary_sensor.demo": {CONF_LINKED_BATTERY_SENSOR: "switch.demo"}},
|
|
{"binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: "switch.demo"}},
|
|
{"binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: -10}},
|
|
{"demo.test": "test"},
|
|
{"demo.test": [1, 2]},
|
|
{"demo.test": None},
|
|
{"demo.test": {CONF_NAME: None}},
|
|
{"media_player.test": {CONF_FEATURE_LIST: [{CONF_FEATURE: "invalid_feature"}]}},
|
|
{
|
|
"media_player.test": {
|
|
CONF_FEATURE_LIST: [
|
|
{CONF_FEATURE: FEATURE_ON_OFF},
|
|
{CONF_FEATURE: FEATURE_ON_OFF},
|
|
]
|
|
}
|
|
},
|
|
{"switch.test": {CONF_TYPE: "invalid_type"}},
|
|
]
|
|
|
|
for conf in configs:
|
|
with pytest.raises(vol.Invalid):
|
|
vec(conf)
|
|
|
|
assert vec({}) == {}
|
|
assert vec({"demo.test": {CONF_NAME: "Name"}}) == {
|
|
"demo.test": {CONF_NAME: "Name", CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
|
|
assert vec(
|
|
{"binary_sensor.demo": {CONF_LINKED_BATTERY_SENSOR: "sensor.demo_battery"}}
|
|
) == {
|
|
"binary_sensor.demo": {
|
|
CONF_LINKED_BATTERY_SENSOR: "sensor.demo_battery",
|
|
CONF_LOW_BATTERY_THRESHOLD: 20,
|
|
}
|
|
}
|
|
assert vec({"binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: 50}}) == {
|
|
"binary_sensor.demo": {CONF_LOW_BATTERY_THRESHOLD: 50}
|
|
}
|
|
|
|
assert vec({"alarm_control_panel.demo": {}}) == {
|
|
"alarm_control_panel.demo": {ATTR_CODE: None, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
assert vec({"alarm_control_panel.demo": {ATTR_CODE: "1234"}}) == {
|
|
"alarm_control_panel.demo": {ATTR_CODE: "1234", CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
|
|
assert vec({"lock.demo": {}}) == {
|
|
"lock.demo": {ATTR_CODE: None, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
assert vec({"lock.demo": {ATTR_CODE: "1234"}}) == {
|
|
"lock.demo": {ATTR_CODE: "1234", CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
|
|
assert vec({"media_player.demo": {}}) == {
|
|
"media_player.demo": {CONF_FEATURE_LIST: {}, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
config = {
|
|
CONF_FEATURE_LIST: [
|
|
{CONF_FEATURE: FEATURE_ON_OFF},
|
|
{CONF_FEATURE: FEATURE_PLAY_PAUSE},
|
|
]
|
|
}
|
|
assert vec({"media_player.demo": config}) == {
|
|
"media_player.demo": {
|
|
CONF_FEATURE_LIST: {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}},
|
|
CONF_LOW_BATTERY_THRESHOLD: 20,
|
|
}
|
|
}
|
|
|
|
assert vec({"switch.demo": {CONF_TYPE: TYPE_FAUCET}}) == {
|
|
"switch.demo": {CONF_TYPE: TYPE_FAUCET, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
assert vec({"switch.demo": {CONF_TYPE: TYPE_OUTLET}}) == {
|
|
"switch.demo": {CONF_TYPE: TYPE_OUTLET, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
assert vec({"switch.demo": {CONF_TYPE: TYPE_SHOWER}}) == {
|
|
"switch.demo": {CONF_TYPE: TYPE_SHOWER, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
assert vec({"switch.demo": {CONF_TYPE: TYPE_SPRINKLER}}) == {
|
|
"switch.demo": {CONF_TYPE: TYPE_SPRINKLER, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
assert vec({"switch.demo": {CONF_TYPE: TYPE_SWITCH}}) == {
|
|
"switch.demo": {CONF_TYPE: TYPE_SWITCH, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
assert vec({"switch.demo": {CONF_TYPE: TYPE_VALVE}}) == {
|
|
"switch.demo": {CONF_TYPE: TYPE_VALVE, CONF_LOW_BATTERY_THRESHOLD: 20}
|
|
}
|
|
|
|
|
|
def test_validate_media_player_features():
|
|
"""Test validate modes for media players."""
|
|
config = {}
|
|
attrs = {ATTR_SUPPORTED_FEATURES: 20873}
|
|
entity_state = State("media_player.demo", "on", attrs)
|
|
assert validate_media_player_features(entity_state, config) is True
|
|
|
|
config = {FEATURE_ON_OFF: None}
|
|
assert validate_media_player_features(entity_state, config) is True
|
|
|
|
entity_state = State("media_player.demo", "on")
|
|
assert validate_media_player_features(entity_state, config) is False
|
|
|
|
|
|
def test_convert_to_float():
|
|
"""Test convert_to_float method."""
|
|
assert convert_to_float(12) == 12
|
|
assert convert_to_float(12.4) == 12.4
|
|
assert convert_to_float(STATE_UNKNOWN) is None
|
|
assert convert_to_float(None) is None
|
|
|
|
|
|
def test_cleanup_name_for_homekit():
|
|
"""Ensure name sanitize works as expected."""
|
|
|
|
assert cleanup_name_for_homekit("abc") == "abc"
|
|
assert cleanup_name_for_homekit("a b c") == "a b c"
|
|
assert cleanup_name_for_homekit("ab_c") == "ab c"
|
|
assert (
|
|
cleanup_name_for_homekit('ab!@#$%^&*()-=":.,><?//\\ frog')
|
|
== "ab--#---&----- -.,------ frog"
|
|
)
|
|
assert cleanup_name_for_homekit("の日本_語文字セット") == "の日本 語文字セット"
|
|
|
|
|
|
def test_temperature_to_homekit():
|
|
"""Test temperature conversion from HA to HomeKit."""
|
|
assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5
|
|
assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4
|
|
|
|
|
|
def test_temperature_to_states():
|
|
"""Test temperature conversion from HomeKit to HA."""
|
|
assert temperature_to_states(20, TEMP_CELSIUS) == 20.0
|
|
assert temperature_to_states(20.2, TEMP_FAHRENHEIT) == 68.5
|
|
|
|
|
|
def test_density_to_air_quality():
|
|
"""Test map PM2.5 density to HomeKit AirQuality level."""
|
|
assert density_to_air_quality(0) == 1
|
|
assert density_to_air_quality(35) == 1
|
|
assert density_to_air_quality(35.1) == 2
|
|
assert density_to_air_quality(75) == 2
|
|
assert density_to_air_quality(115) == 3
|
|
assert density_to_air_quality(150) == 4
|
|
assert density_to_air_quality(300) == 5
|
|
|
|
|
|
async def test_show_setup_msg(hass, hk_driver):
|
|
"""Test show setup message as persistence notification."""
|
|
pincode = b"123-45-678"
|
|
|
|
entry = await async_init_integration(hass)
|
|
assert entry
|
|
|
|
call_create_notification = async_mock_service(
|
|
hass, PERSISTENT_NOTIFICATION_DOMAIN, "create"
|
|
)
|
|
|
|
await hass.async_add_executor_job(
|
|
show_setup_message, hass, entry.entry_id, "bridge_name", pincode, "X-HM://0"
|
|
)
|
|
await hass.async_block_till_done()
|
|
assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR_SECRET]
|
|
assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR]
|
|
|
|
assert call_create_notification
|
|
assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == entry.entry_id
|
|
assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE]
|
|
|
|
|
|
async def test_dismiss_setup_msg(hass):
|
|
"""Test dismiss setup message."""
|
|
call_dismiss_notification = async_mock_service(
|
|
hass, PERSISTENT_NOTIFICATION_DOMAIN, "dismiss"
|
|
)
|
|
|
|
await hass.async_add_executor_job(dismiss_setup_message, hass, "entry_id")
|
|
await hass.async_block_till_done()
|
|
|
|
assert call_dismiss_notification
|
|
assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == "entry_id"
|
|
|
|
|
|
async def test_port_is_available(hass):
|
|
"""Test we can get an available port and it is actually available."""
|
|
next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)
|
|
|
|
assert next_port
|
|
|
|
assert await hass.async_add_executor_job(port_is_available, next_port)
|
|
|
|
|
|
async def test_port_is_available_skips_existing_entries(hass):
|
|
"""Test we can get an available port and it is actually available."""
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_CONFIG_FLOW_PORT},
|
|
options={},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT)
|
|
|
|
assert next_port
|
|
assert next_port != DEFAULT_CONFIG_FLOW_PORT
|
|
|
|
assert await hass.async_add_executor_job(port_is_available, next_port)
|
|
|
|
|
|
async def test_format_sw_version():
|
|
"""Test format_sw_version method."""
|
|
assert format_sw_version("soho+3.6.8+soho-release-rt120+10") == "3.6.8"
|
|
assert format_sw_version("undefined-undefined-1.6.8") == "1.6.8"
|
|
assert format_sw_version("56.0-76060") == "56.0.76060"
|
|
assert format_sw_version(3.6) == "3.6"
|
|
assert format_sw_version("unknown") is None
|
|
|
|
|
|
async def test_accessory_friendly_name():
|
|
"""Test we provide a helpful friendly name."""
|
|
|
|
accessory = Mock()
|
|
accessory.display_name = "same"
|
|
assert accessory_friendly_name("Same", accessory) == "Same"
|
|
assert accessory_friendly_name("hass title", accessory) == "hass title (same)"
|
|
accessory.display_name = "Hass title 123"
|
|
assert accessory_friendly_name("hass title", accessory) == "Hass title 123"
|
|
|
|
|
|
async def test_lock_state_needs_accessory_mode(hass):
|
|
"""Test that locks are setup as accessories."""
|
|
hass.states.async_set("lock.mine", "locked")
|
|
assert state_needs_accessory_mode(hass.states.get("lock.mine")) is True
|