373 lines
11 KiB
Python
373 lines
11 KiB
Python
|
"""The tests for the MQTT discovery."""
|
||
|
import copy
|
||
|
import json
|
||
|
|
||
|
from hatasmota.const import CONF_ONLINE
|
||
|
import pytest
|
||
|
|
||
|
from homeassistant.components.tasmota.const import DEFAULT_PREFIX
|
||
|
from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED
|
||
|
|
||
|
from tests.async_mock import patch
|
||
|
from tests.common import async_fire_mqtt_message
|
||
|
from tests.components.tasmota.conftest import setup_tasmota_helper
|
||
|
|
||
|
DEFAULT_CONFIG = {
|
||
|
"dn": "My Device",
|
||
|
"fn": ["Beer", "Milk", "Three", "Four", "Five"],
|
||
|
"hn": "tasmota_49A3BC",
|
||
|
"mac": "00000049A3BC",
|
||
|
"md": "Sonoff 123",
|
||
|
"ofl": "offline",
|
||
|
CONF_ONLINE: "online",
|
||
|
"state": ["OFF", "ON", "TOGGLE", "HOLD"],
|
||
|
"sw": "2.3.3.4",
|
||
|
"t": "tasmota_49A3BC",
|
||
|
"t_f": "%topic%/%prefix%/",
|
||
|
"t_p": ["cmnd", "stat", "tele"],
|
||
|
"li": [0, 0, 0, 0, 0, 0, 0, 0],
|
||
|
"rl": [0, 0, 0, 0, 0, 0, 0, 0],
|
||
|
"se": [],
|
||
|
"ver": 1,
|
||
|
}
|
||
|
|
||
|
|
||
|
async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota):
|
||
|
"""Test setting up discovery."""
|
||
|
discovery_topic = DEFAULT_PREFIX
|
||
|
|
||
|
assert mqtt_mock.async_subscribe.called
|
||
|
call_args = mqtt_mock.async_subscribe.mock_calls[0][1]
|
||
|
assert call_args[0] == discovery_topic + "/#"
|
||
|
assert call_args[2] == 0
|
||
|
|
||
|
|
||
|
async def test_valid_discovery_message(hass, mqtt_mock, caplog):
|
||
|
"""Test discovery callback called."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
|
||
|
with patch(
|
||
|
"homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
|
||
|
) as mock_tasmota_has_entities:
|
||
|
await setup_tasmota_helper(hass)
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass, f"{DEFAULT_PREFIX}/00000049A3BC/config", json.dumps(config)
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
assert mock_tasmota_has_entities.called
|
||
|
|
||
|
|
||
|
async def test_invalid_topic(hass, mqtt_mock):
|
||
|
"""Test receiving discovery message on wrong topic."""
|
||
|
with patch(
|
||
|
"homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
|
||
|
) as mock_tasmota_has_entities:
|
||
|
await setup_tasmota_helper(hass)
|
||
|
|
||
|
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/123456/configuration", "{}")
|
||
|
await hass.async_block_till_done()
|
||
|
assert not mock_tasmota_has_entities.called
|
||
|
|
||
|
|
||
|
async def test_invalid_message(hass, mqtt_mock, caplog):
|
||
|
"""Test receiving an invalid message."""
|
||
|
with patch(
|
||
|
"homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
|
||
|
) as mock_tasmota_has_entities:
|
||
|
await setup_tasmota_helper(hass)
|
||
|
|
||
|
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/123456/config", "asd")
|
||
|
await hass.async_block_till_done()
|
||
|
assert "Invalid discovery message" in caplog.text
|
||
|
assert not mock_tasmota_has_entities.called
|
||
|
|
||
|
|
||
|
async def test_invalid_mac(hass, mqtt_mock, caplog):
|
||
|
"""Test topic is not matching device MAC."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
|
||
|
with patch(
|
||
|
"homeassistant.components.tasmota.discovery.tasmota_has_entities_with_platform"
|
||
|
) as mock_tasmota_has_entities:
|
||
|
await setup_tasmota_helper(hass)
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass, f"{DEFAULT_PREFIX}/00000049A3BA/config", json.dumps(config)
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
assert "MAC mismatch" in caplog.text
|
||
|
assert not mock_tasmota_has_entities.called
|
||
|
|
||
|
|
||
|
async def test_correct_config_discovery(
|
||
|
hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
|
||
|
):
|
||
|
"""Test receiving valid discovery message."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
config["rl"][0] = 1
|
||
|
mac = config["mac"]
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device and registry entries are created
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
entity_entry = entity_reg.async_get("switch.beer")
|
||
|
assert entity_entry is not None
|
||
|
|
||
|
state = hass.states.get("switch.beer")
|
||
|
assert state is not None
|
||
|
assert state.name == "Beer"
|
||
|
|
||
|
assert (mac, "switch", 0) in hass.data[ALREADY_DISCOVERED]
|
||
|
|
||
|
|
||
|
async def test_device_discover(
|
||
|
hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
|
||
|
):
|
||
|
"""Test setting up a device."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
mac = config["mac"]
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device and registry entries are created
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
assert device_entry.manufacturer == "Tasmota"
|
||
|
assert device_entry.model == config["md"]
|
||
|
assert device_entry.name == config["dn"]
|
||
|
assert device_entry.sw_version == config["sw"]
|
||
|
|
||
|
|
||
|
async def test_device_update(
|
||
|
hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
|
||
|
):
|
||
|
"""Test updating a device."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
config["md"] = "Model 1"
|
||
|
config["dn"] = "Name 1"
|
||
|
config["sw"] = "v1.2.3.4"
|
||
|
mac = config["mac"]
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is created
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
|
||
|
# Update device parameters
|
||
|
config["md"] = "Another model"
|
||
|
config["dn"] = "Another name"
|
||
|
config["sw"] = "v6.6.6"
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is updated
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
assert device_entry.model == "Another model"
|
||
|
assert device_entry.name == "Another name"
|
||
|
assert device_entry.sw_version == "v6.6.6"
|
||
|
|
||
|
|
||
|
@pytest.mark.no_fail_on_log_exception
|
||
|
async def test_discovery_broken(hass, mqtt_mock, caplog, device_reg, setup_tasmota):
|
||
|
"""Test handling of exception when creating discovered device."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
mac = config["mac"]
|
||
|
data = json.dumps(config)
|
||
|
|
||
|
# Trigger an exception when the entity is added
|
||
|
with patch(
|
||
|
"hatasmota.discovery.get_device_config_helper",
|
||
|
return_value=object(),
|
||
|
):
|
||
|
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", data)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is not created
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is None
|
||
|
assert (
|
||
|
"Exception in async_discover_device when dispatching 'tasmota_discovery_device'"
|
||
|
in caplog.text
|
||
|
)
|
||
|
|
||
|
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", data)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is created
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
|
||
|
|
||
|
async def test_device_remove(
|
||
|
hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
|
||
|
):
|
||
|
"""Test removing a discovered device."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
mac = config["mac"]
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is created
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
"",
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is removed
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is None
|
||
|
|
||
|
|
||
|
async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_tasmota):
|
||
|
"""Test removing a stale (undiscovered) device does not throw."""
|
||
|
mac = "00000049A3BC"
|
||
|
|
||
|
config_entry = hass.config_entries.async_entries("tasmota")[0]
|
||
|
|
||
|
# Create a device
|
||
|
device_reg.async_get_or_create(
|
||
|
config_entry_id=config_entry.entry_id,
|
||
|
connections={("mac", mac)},
|
||
|
)
|
||
|
|
||
|
# Verify device entry was created
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
|
||
|
# Remove the device
|
||
|
device_reg.async_remove_device(device_entry.id)
|
||
|
|
||
|
# Verify device entry is removed
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is None
|
||
|
|
||
|
|
||
|
async def test_device_rediscover(
|
||
|
hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota
|
||
|
):
|
||
|
"""Test removing a device."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
mac = config["mac"]
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is created
|
||
|
device_entry1 = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry1 is not None
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
"",
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is removed
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is None
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
# Verify device entry is created, and id is reused
|
||
|
device_entry = device_reg.async_get_device(set(), {("mac", mac)})
|
||
|
assert device_entry is not None
|
||
|
assert device_entry1.id == device_entry.id
|
||
|
|
||
|
|
||
|
async def test_entity_duplicate_discovery(hass, mqtt_mock, caplog, setup_tasmota):
|
||
|
"""Test entities are not duplicated."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
config["rl"][0] = 1
|
||
|
mac = config["mac"]
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
|
||
|
state = hass.states.get("switch.beer")
|
||
|
state_duplicate = hass.states.get("binary_sensor.beer1")
|
||
|
|
||
|
assert state is not None
|
||
|
assert state.name == "Beer"
|
||
|
assert state_duplicate is None
|
||
|
assert (
|
||
|
f"Entity already added, sending update: switch ('{mac}', 'switch', 0)"
|
||
|
in caplog.text
|
||
|
)
|
||
|
|
||
|
|
||
|
async def test_entity_duplicate_removal(hass, mqtt_mock, caplog, setup_tasmota):
|
||
|
"""Test removing entity twice."""
|
||
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
||
|
config["rl"][0] = 1
|
||
|
mac = config["mac"]
|
||
|
|
||
|
async_fire_mqtt_message(
|
||
|
hass,
|
||
|
f"{DEFAULT_PREFIX}/{mac}/config",
|
||
|
json.dumps(config),
|
||
|
)
|
||
|
await hass.async_block_till_done()
|
||
|
config["rl"][0] = 0
|
||
|
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config))
|
||
|
await hass.async_block_till_done()
|
||
|
assert f"Removing entity: switch ('{mac}', 'switch', 0)" in caplog.text
|
||
|
|
||
|
caplog.clear()
|
||
|
async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config))
|
||
|
await hass.async_block_till_done()
|
||
|
assert f"Removing entity: switch ('{mac}', 'switch', 0)" not in caplog.text
|