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:
2026-09-29 12:44:34 +03:00
parent 29f62b639c
commit 3e0b6b3653
21 changed files with 1974 additions and 87 deletions
+66
View File
@@ -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)