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:
@@ -382,9 +382,14 @@ def main():
|
||||
ap.add_argument("--stale-gap", type=float, default=44.0)
|
||||
ap.add_argument("--push-every", type=float, default=0)
|
||||
ap.add_argument("--fail-first-ke", action="store_true")
|
||||
ap.add_argument("--set", action="append", default=[], metavar="NAME=VALUE",
|
||||
help="предустановить свойство mock-модуля (повторяемо)")
|
||||
args = ap.parse_args()
|
||||
|
||||
mock = Mock(args)
|
||||
for item in args.set:
|
||||
name, _, value = item.partition("=")
|
||||
mock.props[name] = int(value, 0) # принимает 0b/0x/десятичное
|
||||
lock = mock.lock
|
||||
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
@@ -13,6 +13,7 @@ from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
import pyfglair
|
||||
from custom_components.fglair.const import DOMAIN
|
||||
from custom_components.fglair.trial import TrialResult
|
||||
from pyfglair import Prop, Value, ValueKind
|
||||
from pyfglair.provision import Device
|
||||
|
||||
MANUAL_INPUT = {
|
||||
@@ -25,6 +26,16 @@ MANUAL_INPUT = {
|
||||
"model": "AP-WC1E",
|
||||
}
|
||||
|
||||
PREVIEW_INPUT = {
|
||||
"template": "A",
|
||||
"temp_min": 16.0,
|
||||
"temp_max": 30.0,
|
||||
"temp_step": 0.5,
|
||||
"temp_num": 1,
|
||||
"temp_den": 1,
|
||||
"temp_offset": 0,
|
||||
}
|
||||
|
||||
TRIAL_OK = TrialResult(ok=True)
|
||||
TRIAL_NETWORK_FAIL = TrialResult(ok=False, reason="unreachable")
|
||||
|
||||
@@ -41,6 +52,10 @@ async def _select(hass: HomeAssistant, flow_id: str, option: str):
|
||||
)
|
||||
|
||||
|
||||
async def _configure(hass: HomeAssistant, flow_id: str, user_input: dict):
|
||||
return await hass.config_entries.flow.async_configure(flow_id, user_input)
|
||||
|
||||
|
||||
def _suggested(schema, field: str):
|
||||
for key in schema.schema:
|
||||
if getattr(key, "schema", None) == field:
|
||||
@@ -64,19 +79,94 @@ async def test_manual_success(hass: HomeAssistant):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
assert result["type"] == "form"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MANUAL_INPUT
|
||||
)
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "preview"
|
||||
result = await _configure(hass, result["flow_id"], PREVIEW_INPUT)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "Bedroom"
|
||||
assert result["result"].unique_id == DSN
|
||||
data = result["data"]
|
||||
assert data["lanip_key_id"] == KEY_ID
|
||||
assert data["template"] == "A"
|
||||
assert data["temp_step"] == 5
|
||||
assert data["overrides"]["adjust_temperature"] == {
|
||||
"num": 1,
|
||||
"den": 1,
|
||||
"offset": 0,
|
||||
"min": 160,
|
||||
"max": 300,
|
||||
}
|
||||
trial.assert_called_once()
|
||||
assert trial.call_args.args[0]["host"] == "192.168.1.50"
|
||||
|
||||
|
||||
async def test_preview_template_change_and_conversion(hass: HomeAssistant):
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TRIAL_OK,
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
preview_input = {
|
||||
**PREVIEW_INPUT,
|
||||
"template": "B",
|
||||
"temp_min": 17.0,
|
||||
"temp_max": 28.0,
|
||||
"temp_step": 1.0,
|
||||
"temp_num": 2,
|
||||
"temp_den": 4,
|
||||
"temp_offset": 5,
|
||||
}
|
||||
result = await _configure(hass, result["flow_id"], preview_input)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"]["template"] == "B"
|
||||
assert result["data"]["temp_step"] == 10
|
||||
assert result["data"]["overrides"]["adjust_temperature"] == {
|
||||
"num": 2,
|
||||
"den": 4,
|
||||
"offset": 5,
|
||||
"min": 170,
|
||||
"max": 280,
|
||||
}
|
||||
|
||||
|
||||
async def test_preview_invalid_range(hass: HomeAssistant):
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TRIAL_OK,
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
result = await _configure(
|
||||
hass, result["flow_id"], {**PREVIEW_INPUT, "temp_min": 30.0, "temp_max": 16.0}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "invalid_range"}
|
||||
result = await _configure(hass, result["flow_id"], PREVIEW_INPUT)
|
||||
assert result["type"] == "create_entry"
|
||||
|
||||
|
||||
async def test_preview_contains_trial_values(hass: HomeAssistant):
|
||||
props = {
|
||||
Prop.DISPLAY_TEMPERATURE: Value(ValueKind.INT, 200),
|
||||
Prop.DEVICE_CAPABILITIES: Value(ValueKind.INT, 0b11011),
|
||||
}
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TrialResult(ok=True, properties=props),
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
preview = result["description_placeholders"]["preview"]
|
||||
assert "20.0" in preview
|
||||
assert "0x0000001B" in preview
|
||||
assert "cool" in preview and "heat" in preview
|
||||
|
||||
|
||||
async def test_manual_connect_error(hass: HomeAssistant):
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
@@ -84,9 +174,7 @@ async def test_manual_connect_error(hass: HomeAssistant):
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MANUAL_INPUT
|
||||
)
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
@@ -98,9 +186,7 @@ async def test_manual_key_mismatch_error(hass: HomeAssistant):
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MANUAL_INPUT
|
||||
)
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "key_mismatch"}
|
||||
|
||||
@@ -114,13 +200,25 @@ async def test_manual_already_configured(hass: HomeAssistant):
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MANUAL_INPUT
|
||||
)
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_manual_error_keeps_input(hass: HomeAssistant):
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TRIAL_NETWORK_FAIL,
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await _configure(hass, result["flow_id"], MANUAL_INPUT)
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
assert _suggested(result["data_schema"], "host") == MANUAL_INPUT["host"]
|
||||
assert _suggested(result["data_schema"], "dsn") == DSN
|
||||
assert _suggested(result["data_schema"], "lanip_key") is None
|
||||
|
||||
|
||||
async def test_import_json_success(hass: HomeAssistant):
|
||||
payload = json.dumps(
|
||||
{
|
||||
@@ -139,8 +237,13 @@ async def test_import_json_success(hass: HomeAssistant):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "import_json")
|
||||
assert result["type"] == "form"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"config_json": payload}
|
||||
result = await _configure(
|
||||
hass, result["flow_id"], {"config_json": payload}
|
||||
)
|
||||
assert result["step_id"] == "preview"
|
||||
assert _suggested(result["data_schema"], "template") == "B"
|
||||
result = await _configure(
|
||||
hass, result["flow_id"], {**PREVIEW_INPUT, "template": "B"}
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"]["host"] == "192.168.1.60"
|
||||
@@ -150,8 +253,8 @@ async def test_import_json_success(hass: HomeAssistant):
|
||||
async def test_import_json_invalid(hass: HomeAssistant):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "import_json")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"config_json": "not json"}
|
||||
result = await _configure(
|
||||
hass, result["flow_id"], {"config_json": "not json"}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "invalid_import"}
|
||||
@@ -176,57 +279,20 @@ async def test_cloud_flow_success(hass: HomeAssistant):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "cloud")
|
||||
assert result["type"] == "form"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result = await _configure(
|
||||
hass,
|
||||
result["flow_id"],
|
||||
{"email": "user@example.com", "password": "secret", "region": "eu"},
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "cloud_device"
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TRIAL_OK,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"dsn": DSN}
|
||||
)
|
||||
assert result["step_id"] == "cloud_device"
|
||||
result = await _configure(hass, result["flow_id"], {"dsn": DSN})
|
||||
assert result["step_id"] == "preview"
|
||||
result = await _configure(hass, result["flow_id"], PREVIEW_INPUT)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"]["host"] == "192.168.1.50"
|
||||
assert result["data"]["lanip_key_id"] == KEY_ID
|
||||
|
||||
|
||||
async def test_cloud_auth_error(hass: HomeAssistant):
|
||||
with patch(
|
||||
"pyfglair.discover",
|
||||
AsyncMock(
|
||||
side_effect=pyfglair.ProvisionError("401 unauthorized", "auth")
|
||||
),
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "cloud")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"email": "user@example.com", "password": "bad", "region": "eu"},
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_manual_error_keeps_input(hass: HomeAssistant):
|
||||
with patch(
|
||||
"custom_components.fglair.config_flow._run_trial",
|
||||
return_value=TRIAL_NETWORK_FAIL,
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "manual")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MANUAL_INPUT
|
||||
)
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
assert _suggested(result["data_schema"], "host") == MANUAL_INPUT["host"]
|
||||
assert _suggested(result["data_schema"], "dsn") == DSN
|
||||
assert _suggested(result["data_schema"], "lanip_key") is None
|
||||
|
||||
|
||||
async def test_cloud_no_devices(hass: HomeAssistant):
|
||||
with patch(
|
||||
"pyfglair.discover",
|
||||
@@ -238,7 +304,8 @@ async def test_cloud_no_devices(hass: HomeAssistant):
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "cloud")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result = await _configure(
|
||||
hass,
|
||||
result["flow_id"],
|
||||
{"email": "user@example.com", "password": "secret", "region": "eu"},
|
||||
)
|
||||
@@ -246,6 +313,24 @@ async def test_cloud_no_devices(hass: HomeAssistant):
|
||||
assert result["errors"] == {"base": "no_devices"}
|
||||
|
||||
|
||||
async def test_cloud_auth_error(hass: HomeAssistant):
|
||||
with patch(
|
||||
"pyfglair.discover",
|
||||
AsyncMock(
|
||||
side_effect=pyfglair.ProvisionError("401 unauthorized", "auth")
|
||||
),
|
||||
):
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "cloud")
|
||||
result = await _configure(
|
||||
hass,
|
||||
result["flow_id"],
|
||||
{"email": "user@example.com", "password": "bad", "region": "eu"},
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_yaml_import(hass: HomeAssistant):
|
||||
payload = {
|
||||
"ip_address": "192.168.1.70",
|
||||
@@ -289,9 +374,10 @@ async def test_import_json_null_device_port(hass: HomeAssistant):
|
||||
) as trial:
|
||||
result = await _menu(hass)
|
||||
result = await _select(hass, result["flow_id"], "import_json")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"config_json": payload}
|
||||
result = await _configure(
|
||||
hass, result["flow_id"], {"config_json": payload}
|
||||
)
|
||||
result = await _configure(hass, result["flow_id"], PREVIEW_INPUT)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["data"]["device_port"] == 80
|
||||
assert trial.call_args.args[0]["device_port"] == 80
|
||||
|
||||
@@ -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)
|
||||
@@ -17,13 +17,14 @@ async def test_setup_unload(hass: HomeAssistant, config_entry, socket_enabled):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = config_entry.runtime_data
|
||||
assert client is not None
|
||||
assert isinstance(client.state, State)
|
||||
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 client.cached(Prop.OPERATION_MODE) is None
|
||||
assert runtime.client.cached(Prop.OPERATION_MODE) is None
|
||||
|
||||
|
||||
async def test_two_entries_default_port(hass: HomeAssistant, entry_data, socket_enabled):
|
||||
@@ -44,9 +45,9 @@ async def test_two_entries_default_port(hass: HomeAssistant, entry_data, socket_
|
||||
assert first.state.name == "LOADED"
|
||||
assert second.state.name == "LOADED"
|
||||
|
||||
first_client = first.runtime_data
|
||||
first_runtime = first.runtime_data
|
||||
assert await hass.config_entries.async_unload(first.entry_id)
|
||||
assert first_client.cached(Prop.OPERATION_MODE) is None
|
||||
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)
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Ручные конверсии/диапазоны (ConversionSet) без HA-рантайма."""
|
||||
from __future__ import annotations
|
||||
|
||||
from custom_components.fglair.overrides import ConversionSet, LinearOverride
|
||||
from pyfglair import Prop, Template
|
||||
|
||||
|
||||
def test_identity_keeps_api_units():
|
||||
conversions = ConversionSet(Template.A, {}, 5)
|
||||
assert conversions.to_display(Prop.DISPLAY_TEMPERATURE, 200) == 200
|
||||
assert conversions.to_display(Prop.ADJUST_TEMPERATURE, 220) == 220
|
||||
assert conversions.from_display(Prop.ADJUST_TEMPERATURE, 220) == 220
|
||||
assert conversions.temperature_range() == (160, 300)
|
||||
|
||||
|
||||
def test_linear_override_roundtrip_and_clamp():
|
||||
override = LinearOverride(
|
||||
num=1, den=1, offset=5, min_value=170, max_value=280
|
||||
)
|
||||
conversions = ConversionSet(
|
||||
Template.A, {Prop.ADJUST_TEMPERATURE: override}, 5
|
||||
)
|
||||
assert conversions.to_display(Prop.ADJUST_TEMPERATURE, 220) == 225
|
||||
assert conversions.from_display(Prop.ADJUST_TEMPERATURE, 225) == 220
|
||||
assert conversions.clamp(Prop.ADJUST_TEMPERATURE, 100) == 170
|
||||
assert conversions.clamp(Prop.ADJUST_TEMPERATURE, 999) == 280
|
||||
assert conversions.temperature_range() == (170, 280)
|
||||
|
||||
|
||||
def test_linear_override_scaled():
|
||||
override = LinearOverride(num=2, den=4, offset=0)
|
||||
conversions = ConversionSet(
|
||||
Template.A, {Prop.ADJUST_TEMPERATURE: override}, 5
|
||||
)
|
||||
assert conversions.to_display(Prop.ADJUST_TEMPERATURE, 200) == 100
|
||||
assert conversions.from_display(Prop.ADJUST_TEMPERATURE, 100) == 200
|
||||
|
||||
|
||||
def test_from_entry_parses_spec():
|
||||
conversions = ConversionSet.from_entry(
|
||||
{
|
||||
"template": "B",
|
||||
"temp_step": 10,
|
||||
"overrides": {
|
||||
"adjust_temperature": {
|
||||
"num": 2,
|
||||
"den": 4,
|
||||
"offset": 5,
|
||||
"min": 170,
|
||||
"max": 280,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
assert conversions.template == Template.B
|
||||
assert conversions.step_tenths == 10
|
||||
assert conversions.temperature_range() == (170, 280)
|
||||
override = conversions.override(Prop.ADJUST_TEMPERATURE)
|
||||
assert (override.num, override.den, override.offset) == (2, 4, 5)
|
||||
assert conversions.override(Prop.FAN_SPEED).identity
|
||||
|
||||
|
||||
def test_from_entry_defaults():
|
||||
conversions = ConversionSet.from_entry({"template": "F"})
|
||||
assert conversions.step_tenths == 5
|
||||
assert conversions.temperature_range() == (160, 300)
|
||||
Reference in New Issue
Block a user