Files
petr.polezhaev 3e0b6b3653 ha(H3): превью шаблона с конверсиями; climate и сущности
- config flow: шаг preview (шаблон A/B/F, диапазон/шаг уставки, линейные
  коэффициенты num/den/offset, текст-превью по данным trial: hvac/fan/
  swing/capabilities/текущая температура)
- coordinator: push-driven DataUpdateCoordinator (снимок кэша, доступность
  online/recovering, первичный GET-батч, один batch_commit на действие)
- overrides.py: ConversionSet (ручные линейные конверсии и диапазон поверх
  таблиц ядра, применяются в HA)
- entity.py: DeviceInfo/unique_id/доступность, capabilities-фильтры
- climate: hvac/fan/swing/vertical+horizontal swing/preset ECO-BOOST,
  уставка 16-30 °C (шаг по шаблону), current_temperature
- sensor: room_temperature, error_code, connection_state (диагностика)
- binary_sensor: connectivity + флаги op_status (PROTOCOL §8.4)
- switch: 8 флагов-свойств; select: положения заслонок 0..N-1
- тесты: превью/конверсии/ошибки диапазона, сущности против mock_ac
  (снимок, команды, capabilities-фильтр, ручная конверсия), ConversionSet
- mock_ac: --set NAME=VALUE (0b/0x) для сценариев
- план: demand_control отложен (семантика не зафиксирована)
2026-09-29 12:44:34 +03:00

121 lines
4.1 KiB
Python

"""Setup/unload ConfigEntry: клиент стартует, останавливается, ошибки порта."""
from __future__ import annotations
import asyncio
import socket
from helpers import free_port
from homeassistant.core import HomeAssistant
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.fglair.const import CONF_LISTEN_PORT, DOMAIN
from pyfglair import Prop, State
async def test_setup_unload(hass: HomeAssistant, config_entry, socket_enabled):
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
runtime = config_entry.runtime_data
assert runtime is not None
assert isinstance(runtime.client.state, State)
assert runtime.coordinator.data is not None
assert await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert runtime.client.cached(Prop.OPERATION_MODE) is None
async def test_two_entries_default_port(hass: HomeAssistant, entry_data, socket_enabled):
"""Две записи без явного listen_port не конфликтуют (порт 0 → эфемерный)."""
first = MockConfigEntry(domain=DOMAIN, data=entry_data, unique_id="DSN-1")
second = MockConfigEntry(
domain=DOMAIN,
data={**entry_data, "dsn": "DSN-2"},
unique_id="DSN-2",
)
first.add_to_hass(hass)
assert await hass.config_entries.async_setup(first.entry_id)
await hass.async_block_till_done()
second.add_to_hass(hass)
assert await hass.config_entries.async_setup(second.entry_id)
await hass.async_block_till_done()
assert first.state.name == "LOADED"
assert second.state.name == "LOADED"
first_runtime = first.runtime_data
assert await hass.config_entries.async_unload(first.entry_id)
assert first_runtime.client.cached(Prop.OPERATION_MODE) is None
assert second.state.name == "LOADED"
assert await hass.config_entries.async_unload(second.entry_id)
async def test_two_entries_same_explicit_port(
hass: HomeAssistant, entry_data, socket_enabled
):
"""Явно заданный один порт на две записи — вторая SETUP_RETRY."""
port = free_port()
first = MockConfigEntry(
domain=DOMAIN,
data={**entry_data, CONF_LISTEN_PORT: port},
unique_id="DSN-1",
)
first.add_to_hass(hass)
assert await hass.config_entries.async_setup(first.entry_id)
await hass.async_block_till_done()
second = MockConfigEntry(
domain=DOMAIN,
data={**entry_data, "dsn": "DSN-2", CONF_LISTEN_PORT: port},
unique_id="DSN-2",
)
second.add_to_hass(hass)
assert not await hass.config_entries.async_setup(second.entry_id)
await hass.async_block_till_done()
assert second.state.name == "SETUP_RETRY"
assert await hass.config_entries.async_unload(first.entry_id)
async def test_unload_does_not_block_loop(
hass: HomeAssistant, config_entry, socket_enabled
):
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
ticks = 0
async def ticker() -> None:
nonlocal ticks
while True:
await asyncio.sleep(0.01)
ticks += 1
task = asyncio.create_task(ticker())
await asyncio.sleep(0.05)
assert await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
task.cancel()
assert ticks >= 3
async def test_setup_port_busy(hass: HomeAssistant, entry_data, socket_enabled):
port = free_port()
sock = socket.socket()
sock.bind(("127.0.0.1", port))
sock.listen(1)
try:
entry = MockConfigEntry(
domain=DOMAIN,
data={**entry_data, CONF_LISTEN_PORT: port},
unique_id=entry_data["dsn"],
)
entry.add_to_hass(hass)
assert not await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state.name == "SETUP_RETRY"
finally:
sock.close()