mirror of
https://github.com/home-assistant/core.git
synced 2026-09-18 14:38:02 +01:00
213 lines
6.5 KiB
Python
213 lines
6.5 KiB
Python
"""Tests for the Modbus services."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from homeassistant import config as hass_config
|
|
from homeassistant.components.modbus.const import (
|
|
ATTR_ADDRESS,
|
|
ATTR_HUB,
|
|
ATTR_VALUE,
|
|
DATA_MODBUS_HUBS,
|
|
DEFAULT_HUB,
|
|
DOMAIN,
|
|
SERVICE_STOP,
|
|
SERVICE_WRITE_COIL,
|
|
SERVICE_WRITE_REGISTER,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_STATE,
|
|
CONF_ADDRESS,
|
|
CONF_HOST,
|
|
CONF_NAME,
|
|
CONF_PORT,
|
|
CONF_SENSORS,
|
|
CONF_TYPE,
|
|
SERVICE_RELOAD,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ServiceValidationError
|
|
from homeassistant.helpers.entity_platform import async_get_platforms
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from tests.common import get_fixture_path
|
|
|
|
HUB_CONFIG = {
|
|
DOMAIN: [
|
|
{
|
|
CONF_NAME: DEFAULT_HUB,
|
|
CONF_TYPE: "tcp",
|
|
CONF_HOST: "modbusHost",
|
|
CONF_PORT: 5501,
|
|
CONF_SENSORS: [{CONF_NAME: "dummy", CONF_ADDRESS: 9999}],
|
|
}
|
|
]
|
|
}
|
|
|
|
SERVICES = (
|
|
SERVICE_RELOAD,
|
|
SERVICE_WRITE_REGISTER,
|
|
SERVICE_WRITE_COIL,
|
|
SERVICE_STOP,
|
|
)
|
|
|
|
# The actions that need a configured hub, and a minimal valid payload for each.
|
|
HUB_SERVICES = [
|
|
pytest.param(
|
|
SERVICE_WRITE_REGISTER,
|
|
{ATTR_HUB: DEFAULT_HUB, ATTR_ADDRESS: 1, ATTR_VALUE: 1},
|
|
id="write_register",
|
|
),
|
|
pytest.param(
|
|
SERVICE_WRITE_COIL,
|
|
{ATTR_HUB: DEFAULT_HUB, ATTR_ADDRESS: 1, ATTR_STATE: True},
|
|
id="write_coil",
|
|
),
|
|
pytest.param(SERVICE_STOP, {ATTR_HUB: DEFAULT_HUB}, id="stop"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("service", SERVICES)
|
|
async def test_services_registered_without_yaml(
|
|
hass: HomeAssistant, service: str
|
|
) -> None:
|
|
"""Test the actions are registered without a Modbus YAML section."""
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert hass.services.has_service(DOMAIN, service)
|
|
|
|
|
|
@pytest.mark.parametrize(("service", "data"), HUB_SERVICES)
|
|
async def test_service_without_yaml_raises(
|
|
hass: HomeAssistant, service: str, data: dict
|
|
) -> None:
|
|
"""Test the hub actions raise without a Modbus YAML section."""
|
|
assert await async_setup_component(hass, DOMAIN, {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert DATA_MODBUS_HUBS not in hass.data
|
|
|
|
with pytest.raises(ServiceValidationError) as err:
|
|
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
|
|
|
assert err.value.translation_domain == DOMAIN
|
|
assert err.value.translation_key == "not_loaded"
|
|
assert "Modbus is not loaded" in str(err.value)
|
|
|
|
|
|
@pytest.mark.parametrize(("service", "data"), HUB_SERVICES)
|
|
async def test_service_without_hubs_raises(
|
|
hass: HomeAssistant, service: str, data: dict
|
|
) -> None:
|
|
"""Test the hub actions raise when Modbus is configured without any hub."""
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: []})
|
|
await hass.async_block_till_done()
|
|
|
|
assert hass.data[DATA_MODBUS_HUBS] == {}
|
|
|
|
with pytest.raises(ServiceValidationError) as err:
|
|
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
|
|
|
assert err.value.translation_key == "not_loaded"
|
|
|
|
|
|
@pytest.mark.parametrize(("service", "data"), HUB_SERVICES)
|
|
async def test_service_after_failed_setup_raises(
|
|
hass: HomeAssistant, service: str, data: dict
|
|
) -> None:
|
|
"""Test the hub actions raise after a failed hub setup.
|
|
|
|
Hubs are stored before their setup is awaited, so a failure must drop them
|
|
again; stop would otherwise raise AttributeError on the unset _connect_task.
|
|
"""
|
|
with patch(
|
|
"homeassistant.components.modbus.modbus.ModbusHub.async_setup",
|
|
return_value=False,
|
|
):
|
|
assert not await async_setup_component(hass, DOMAIN, HUB_CONFIG)
|
|
await hass.async_block_till_done()
|
|
|
|
assert DATA_MODBUS_HUBS not in hass.data
|
|
|
|
with pytest.raises(ServiceValidationError) as err:
|
|
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
|
|
|
assert err.value.translation_key == "not_loaded"
|
|
|
|
|
|
@pytest.mark.parametrize(("service", "data"), HUB_SERVICES)
|
|
async def test_service_after_failed_reload_raises(
|
|
hass: HomeAssistant, service: str, data: dict
|
|
) -> None:
|
|
"""Test the hub actions raise after a reload failed to set the hubs up."""
|
|
assert await async_setup_component(hass, DOMAIN, HUB_CONFIG)
|
|
await hass.async_block_till_done()
|
|
assert hass.data[DATA_MODBUS_HUBS]
|
|
|
|
yaml_path = get_fixture_path("configuration.yaml", DOMAIN)
|
|
with (
|
|
patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path),
|
|
patch(
|
|
"homeassistant.components.modbus.modbus.ModbusHub.async_setup",
|
|
return_value=False,
|
|
),
|
|
):
|
|
await hass.services.async_call(DOMAIN, SERVICE_RELOAD, {}, blocking=True)
|
|
await hass.async_block_till_done()
|
|
|
|
assert DATA_MODBUS_HUBS not in hass.data
|
|
|
|
with pytest.raises(ServiceValidationError) as err:
|
|
await hass.services.async_call(DOMAIN, service, data, blocking=True)
|
|
|
|
assert err.value.translation_key == "not_loaded"
|
|
|
|
|
|
TEST_HUB_A = "hub_a"
|
|
TEST_HUB_B = "hub_b"
|
|
|
|
|
|
def _two_hub_config() -> dict:
|
|
"""Return a config with two hubs, each exposing one sensor."""
|
|
return {
|
|
DOMAIN: [
|
|
{
|
|
CONF_NAME: name,
|
|
CONF_TYPE: "tcp",
|
|
CONF_HOST: "modbusHost",
|
|
CONF_PORT: port,
|
|
CONF_SENSORS: [{CONF_NAME: f"sensor_{name}", CONF_ADDRESS: 1}],
|
|
}
|
|
for name, port in ((TEST_HUB_A, 5501), (TEST_HUB_B, 5502))
|
|
]
|
|
}
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_pymodbus")
|
|
async def test_stop_only_disables_the_selected_hub(hass: HomeAssistant) -> None:
|
|
"""Test stopping one hub leaves the other hub's entities alone."""
|
|
assert await async_setup_component(hass, DOMAIN, _two_hub_config())
|
|
await hass.async_block_till_done()
|
|
|
|
# async_disable only flips availability, it does not write the state, so the
|
|
# entity objects rather than the state machine show the effect.
|
|
entities = {
|
|
entity.entity_id: entity
|
|
for platform in async_get_platforms(hass, DOMAIN)
|
|
for entity in platform.entities.values()
|
|
}
|
|
entity_a = entities[f"sensor.sensor_{TEST_HUB_A}"]
|
|
entity_b = entities[f"sensor.sensor_{TEST_HUB_B}"]
|
|
assert entity_a.available
|
|
assert entity_b.available
|
|
|
|
await hass.services.async_call(
|
|
DOMAIN, SERVICE_STOP, {ATTR_HUB: TEST_HUB_A}, blocking=True
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert not entity_a.available
|
|
assert entity_b.available
|