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 отложен (семантика не зафиксирована)
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
"""Сущности fglair против mock_ac.py: climate/switch/select/sensor/binary."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from helpers import DSN, KEY_ID, LANIP_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.fglair.const import DOMAIN
|
||||
from pyfglair import State
|
||||
|
||||
MOCK_SETTINGS = [
|
||||
"--set", "operation_mode=6",
|
||||
"--set", "fan_speed=4",
|
||||
"--set", "adjust_temperature=220",
|
||||
"--set", "display_temperature=7000",
|
||||
"--set", "device_capabilities=0b1111111111111111111",
|
||||
"--set", "af_vertical_num_dir=5",
|
||||
"--set", "af_horizontal_num_dir=7",
|
||||
"--set", "af_vertical_direction=2",
|
||||
"--set", "af_horizontal_direction=3",
|
||||
"--set", "op_status=0b1000000010000000000000000",
|
||||
"--set", "economy_mode=0",
|
||||
"--set", "powerful_mode=0",
|
||||
]
|
||||
|
||||
|
||||
async def _wait_for(predicate, timeout: float = 20.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
result = predicate()
|
||||
if result:
|
||||
return result
|
||||
await asyncio.sleep(0.1)
|
||||
raise AssertionError("условие не выполнено за отведённое время")
|
||||
|
||||
|
||||
def _entity_id(hass: HomeAssistant, platform: str, unique_id: str):
|
||||
return er.async_get(hass).async_get_entity_id(platform, DOMAIN, unique_id)
|
||||
|
||||
|
||||
async def _setup_entry(hass: HomeAssistant, port: int) -> MockConfigEntry:
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"host": "127.0.0.1",
|
||||
"device_port": port,
|
||||
"dsn": DSN,
|
||||
"lanip_key": LANIP_KEY,
|
||||
"lanip_key_id": KEY_ID,
|
||||
"template": "A",
|
||||
"name": "Mock AC",
|
||||
"listen_port": 0,
|
||||
},
|
||||
unique_id=DSN,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return entry
|
||||
|
||||
|
||||
async def test_entities_snapshot(hass: HomeAssistant, mock_ac, socket_enabled):
|
||||
proc = mock_ac(MOCK_SETTINGS)
|
||||
entry = await _setup_entry(hass, proc.port)
|
||||
runtime = entry.runtime_data
|
||||
|
||||
await _wait_for(lambda: runtime.client.state == State.ONLINE)
|
||||
climate_id = _entity_id(hass, "climate", f"{DSN}_climate")
|
||||
await _wait_for(
|
||||
lambda: climate_id and hass.states.get(climate_id) is not None
|
||||
and hass.states.get(climate_id).state == "heat"
|
||||
and hass.states.get(climate_id).attributes["swing_mode"] == "off"
|
||||
)
|
||||
|
||||
climate = hass.states.get(climate_id)
|
||||
assert climate.attributes["current_temperature"] == 20.0
|
||||
assert climate.attributes["temperature"] == 22.0
|
||||
assert climate.attributes["fan_mode"] == "auto"
|
||||
assert climate.attributes["preset_mode"] is None
|
||||
assert "heat" in climate.attributes["hvac_modes"]
|
||||
assert "cool" in climate.attributes["hvac_modes"]
|
||||
assert climate.attributes["swing_mode"] == "off"
|
||||
|
||||
room = _entity_id(hass, "sensor", f"{DSN}_display_temperature")
|
||||
await _wait_for(
|
||||
lambda: room and hass.states.get(room) is not None
|
||||
and hass.states.get(room).state == "20.0"
|
||||
)
|
||||
|
||||
connectivity = _entity_id(hass, "binary_sensor", f"{DSN}_connectivity")
|
||||
assert hass.states.get(connectivity).state == "on"
|
||||
|
||||
defrost = _entity_id(hass, "binary_sensor", f"{DSN}_op_status_defrost")
|
||||
await _wait_for(lambda: hass.states.get(defrost).state == "on")
|
||||
maintenance = _entity_id(
|
||||
hass, "binary_sensor", f"{DSN}_op_status_maintenance"
|
||||
)
|
||||
assert hass.states.get(maintenance).state == "off"
|
||||
|
||||
vertical = _entity_id(
|
||||
hass, "select", f"{DSN}_af_vertical_direction"
|
||||
)
|
||||
await _wait_for(
|
||||
lambda: hass.states.get(vertical).attributes["options"]
|
||||
== ["0", "1", "2", "3", "4"]
|
||||
)
|
||||
vertical_state = hass.states.get(vertical)
|
||||
assert vertical_state.state == "2"
|
||||
|
||||
economy = _entity_id(hass, "switch", f"{DSN}_economy_mode")
|
||||
assert hass.states.get(economy).state == "off"
|
||||
|
||||
connection = _entity_id(hass, "sensor", f"{DSN}_connection_state")
|
||||
assert hass.states.get(connection).state == "online"
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_entity_commands(hass: HomeAssistant, mock_ac, socket_enabled):
|
||||
proc = mock_ac(MOCK_SETTINGS)
|
||||
entry = await _setup_entry(hass, proc.port)
|
||||
runtime = entry.runtime_data
|
||||
await _wait_for(lambda: runtime.client.state == State.ONLINE)
|
||||
climate_id = _entity_id(hass, "climate", f"{DSN}_climate")
|
||||
select_id = _entity_id(hass, "select", f"{DSN}_af_vertical_direction")
|
||||
await _wait_for(
|
||||
lambda: climate_id and hass.states.get(climate_id) is not None
|
||||
and hass.states.get(climate_id).state == "heat"
|
||||
and hass.states.get(select_id).attributes["options"]
|
||||
== ["0", "1", "2", "3", "4"]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_hvac_mode",
|
||||
{"entity_id": climate_id, "hvac_mode": "cool"},
|
||||
blocking=True,
|
||||
)
|
||||
assert proc.wait_line("CMD SET operation_mode=3", 10)
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{"entity_id": climate_id, "temperature": 25.5},
|
||||
blocking=True,
|
||||
)
|
||||
assert proc.wait_line("CMD SET adjust_temperature=255", 10)
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_fan_mode",
|
||||
{"entity_id": climate_id, "fan_mode": "low"},
|
||||
blocking=True,
|
||||
)
|
||||
assert proc.wait_line("CMD SET fan_speed=1", 10)
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_preset_mode",
|
||||
{"entity_id": climate_id, "preset_mode": "eco"},
|
||||
blocking=True,
|
||||
)
|
||||
assert proc.wait_line("CMD SET economy_mode=True", 10)
|
||||
assert proc.wait_line("CMD SET powerful_mode=False", 10)
|
||||
|
||||
await _wait_for(
|
||||
lambda: hass.states.get(climate_id).state == "cool"
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"select",
|
||||
"select_option",
|
||||
{"entity_id": select_id, "option": "4"},
|
||||
blocking=True,
|
||||
)
|
||||
assert proc.wait_line("CMD SET af_vertical_direction=4", 10)
|
||||
|
||||
switch_id = _entity_id(hass, "switch", f"{DSN}_economy_mode")
|
||||
await hass.services.async_call(
|
||||
"switch", "turn_on", {"entity_id": switch_id}, blocking=True
|
||||
)
|
||||
assert proc.wait_line("CMD SET economy_mode=True", 10)
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
RESTRICTED_SETTINGS = [
|
||||
"--set", "operation_mode=3",
|
||||
"--set", "fan_speed=4",
|
||||
"--set", "device_capabilities=0b100001", # cool (0) + fan auto (5)
|
||||
]
|
||||
|
||||
|
||||
async def test_capabilities_filter(
|
||||
hass: HomeAssistant, mock_ac, socket_enabled
|
||||
):
|
||||
proc = mock_ac(RESTRICTED_SETTINGS)
|
||||
entry = await _setup_entry(hass, proc.port)
|
||||
runtime = entry.runtime_data
|
||||
await _wait_for(lambda: runtime.client.state == State.ONLINE)
|
||||
climate_id = _entity_id(hass, "climate", f"{DSN}_climate")
|
||||
await _wait_for(
|
||||
lambda: climate_id and hass.states.get(climate_id) is not None
|
||||
and hass.states.get(climate_id).state == "cool"
|
||||
and "heat" not in hass.states.get(climate_id).attributes["hvac_modes"]
|
||||
)
|
||||
climate = hass.states.get(climate_id)
|
||||
assert "heat" not in climate.attributes["hvac_modes"]
|
||||
assert "cool" in climate.attributes["hvac_modes"]
|
||||
assert climate.attributes["fan_modes"] == ["auto"]
|
||||
assert climate.attributes.get("preset_modes") is None
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_manual_conversion_applied(
|
||||
hass: HomeAssistant, mock_ac, socket_enabled
|
||||
):
|
||||
settings = [
|
||||
"--set", "operation_mode=6",
|
||||
"--set", "adjust_temperature=220",
|
||||
"--set", "display_temperature=7000",
|
||||
]
|
||||
proc = mock_ac(settings)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"host": "127.0.0.1",
|
||||
"device_port": proc.port,
|
||||
"dsn": DSN,
|
||||
"lanip_key": LANIP_KEY,
|
||||
"lanip_key_id": KEY_ID,
|
||||
"template": "A",
|
||||
"name": "Mock AC",
|
||||
"listen_port": 0,
|
||||
"temp_step": 5,
|
||||
"overrides": {
|
||||
"adjust_temperature": {
|
||||
"num": 1,
|
||||
"den": 1,
|
||||
"offset": 5,
|
||||
"min": 170,
|
||||
"max": 280,
|
||||
}
|
||||
},
|
||||
},
|
||||
unique_id=DSN,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
runtime = entry.runtime_data
|
||||
await _wait_for(lambda: runtime.client.state == State.ONLINE)
|
||||
climate_id = _entity_id(hass, "climate", f"{DSN}_climate")
|
||||
await _wait_for(
|
||||
lambda: climate_id and hass.states.get(climate_id) is not None
|
||||
and hass.states.get(climate_id).state == "heat"
|
||||
)
|
||||
assert hass.states.get(climate_id).attributes["temperature"] == 22.5
|
||||
assert hass.states.get(climate_id).attributes["min_temp"] == 17.0
|
||||
assert hass.states.get(climate_id).attributes["max_temp"] == 28.0
|
||||
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
"set_temperature",
|
||||
{"entity_id": climate_id, "temperature": 25.0},
|
||||
blocking=True,
|
||||
)
|
||||
assert proc.wait_line("CMD SET adjust_temperature=245", 10)
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
Reference in New Issue
Block a user