ha(ux): мастер из трёх страниц, feature-модель и capabilities-фильтрация

- config flow: template (A/B/F с пояснениями, значения с устройства,
  определение по oem_model; для неизвестных моделей — пробинг A/B/F тремя
  короткими сессиями) -> capabilities (23 тумблера с описаниями, дефолты
  из device_capabilities/num_dir/ответов) -> limits (диапазон/шаг, ручная
  конверсия); каждая страница с пометкой «значения определены
  автоматически, можно пропустить»
- features.py: FeatureSet/LiveValues/device_default/resolve_features;
  entry.data["features"] хранит выбор пользователя; reconfigure
  предзаполняет; YAML-import получает дефолты
- сущности создаются только для включённых фич: climate (режимы/скорости/
  swing/пресеты), switch, select заслонок (caps+num_dir), датчик комнаты;
  исправлено появление select без ламелей
- trial: caps/num_dir и presence-зонды (необязательные свойства), answered;
  координатор ждёт первые свойства перед созданием сущностей (feature-
  дефолты без гонки)
- атрибут description у сущностей (HA не поддерживает тултипы) + описания
  фич в мастере (en/ru)
- тесты: features, probing, обновлённые flow/entities/repairs; 57 components
This commit is contained in:
2026-09-29 17:39:34 +03:00
parent 4ee428a6fd
commit d1c02d7c4b
20 changed files with 1131 additions and 212 deletions
+126
View File
@@ -24,6 +24,7 @@ CONF_PASSWORD = "password"
CONF_CONFIG_JSON = "config_json"
CONF_OVERRIDES = "overrides"
CONF_TEMP_STEP = "temp_step"
CONF_FEATURES = "features"
CONF_TEMP_MIN = "temp_min"
CONF_TEMP_MAX = "temp_max"
CONF_TEMP_NUM = "temp_num"
@@ -112,6 +113,131 @@ SWITCH_PROPS: dict[Prop, tuple[str, int | None]] = {
Prop.INDOOR_FAN_CONTROL: ("indoor_fan_control", CAP_INDOOR_FAN_CONTROL),
}
# ---------------------------------------------------------------------------
# Feature-модель (план: страница capabilities в мастере + фильтрация сущностей).
# key -> (capability bit | None, свойство-источник | None).
# Для битовых фич дефолт = бит в device_capabilities; для небитовых —
# наличие свойства в ответах устройства; для заслонок — num_dir > 1.
# ---------------------------------------------------------------------------
FEATURES: dict[str, tuple[int | None, Prop | None]] = {
"mode_cool": (CAP_HVAC[HVACMode.COOL], Prop.OPERATION_MODE),
"mode_dry": (CAP_HVAC[HVACMode.DRY], Prop.OPERATION_MODE),
"mode_fan": (CAP_HVAC[HVACMode.FAN_ONLY], Prop.OPERATION_MODE),
"mode_heat": (CAP_HVAC[HVACMode.HEAT], Prop.OPERATION_MODE),
"mode_auto": (CAP_HVAC[HVACMode.AUTO], Prop.OPERATION_MODE),
"fan_quiet": (CAP_FAN["quiet"], Prop.FAN_SPEED),
"fan_low": (CAP_FAN["low"], Prop.FAN_SPEED),
"fan_medium": (CAP_FAN["medium"], Prop.FAN_SPEED),
"fan_high": (CAP_FAN["high"], Prop.FAN_SPEED),
"fan_auto": (CAP_FAN["auto"], Prop.FAN_SPEED),
"swing_vertical": (CAP_SWING_VERTICAL, Prop.AF_VERTICAL_SWING),
"swing_horizontal": (CAP_SWING_HORIZONTAL, Prop.AF_HORIZONTAL_SWING),
"economy_mode": (CAP_ECONOMY, Prop.ECONOMY_MODE),
"min_heat": (CAP_MIN_HEAT, Prop.MIN_HEAT),
"indoor_fan_control": (CAP_INDOOR_FAN_CONTROL, Prop.INDOOR_FAN_CONTROL),
"powerful_mode": (CAP_POWERFUL, Prop.POWERFUL_MODE),
"outdoor_low_noise": (CAP_OUTDOOR_LOW_NOISE, Prop.OUTDOOR_LOW_NOISE),
"coil_dry_mode": (CAP_COIL_DRY, Prop.COIL_DRY_MODE),
"display_temperature": (None, Prop.DISPLAY_TEMPERATURE),
"human_det_auto_save": (None, Prop.HUMAN_DET_AUTO_SAVE),
"wifi_led_enable": (None, Prop.WIFI_LED_ENABLE),
"af_vertical_direction": (None, Prop.AF_VERTICAL_DIRECTION),
"af_horizontal_direction": (None, Prop.AF_HORIZONTAL_DIRECTION),
}
# Описания для мастера и для атрибута description сущностей.
FEATURE_DESCRIPTIONS = {
"ru": {
"mode_cool": "Охлаждение",
"mode_dry": "Осушение",
"mode_fan": "Вентиляция (без нагрева/охлаждения)",
"mode_heat": "Обогрев",
"mode_auto": "Автоматический режим",
"fan_auto": "Автоскорость вентилятора",
"fan_quiet": "Тихая скорость",
"fan_low": "Низкая скорость",
"fan_medium": "Средняя скорость",
"fan_high": "Высокая скорость",
"swing_vertical": "Качание заслонки по вертикали",
"swing_horizontal": "Качание заслонки по горизонтали",
"economy_mode": "Экономичный режим",
"min_heat": "Минимальный обогрев (поддержание +10 °C)",
"indoor_fan_control": "Управление вентилятором при работе наружного блока",
"powerful_mode": "Мощный режим (ускоренный выход на режим)",
"outdoor_low_noise": "Тихий наружный блок",
"coil_dry_mode": "Сушка испарителя",
"display_temperature": "Датчик температуры помещения",
"human_det_auto_save": "Автосохранение при отсутствии людей (датчик присутствия)",
"wifi_led_enable": "Индикатор Wi-Fi на модуле",
"af_vertical_direction": "Выбор положения вертикальной заслонки",
"af_horizontal_direction": "Выбор положения горизонтальной заслонки",
},
"en": {
"mode_cool": "Cooling",
"mode_dry": "Dry",
"mode_fan": "Fan only",
"mode_heat": "Heating",
"mode_auto": "Automatic mode",
"fan_auto": "Auto fan speed",
"fan_quiet": "Quiet fan speed",
"fan_low": "Low fan speed",
"fan_medium": "Medium fan speed",
"fan_high": "High fan speed",
"swing_vertical": "Vertical louver swing",
"swing_horizontal": "Horizontal louver swing",
"economy_mode": "Economy mode",
"min_heat": "Minimum heat (keep +10 °C)",
"indoor_fan_control": "Indoor fan control while outdoor unit runs",
"powerful_mode": "Powerful mode",
"outdoor_low_noise": "Outdoor unit low noise",
"coil_dry_mode": "Coil dry",
"display_temperature": "Room temperature sensor",
"human_det_auto_save": "Human detection auto save",
"wifi_led_enable": "Wi-Fi LED indicator",
"af_vertical_direction": "Vertical louver position",
"af_horizontal_direction": "Horizontal louver position",
},
}
ENTITY_DESCRIPTIONS = {
"ru": {
"climate": "Управление кондиционером: режим, скорость, уставка, "
"заслонки, пресеты",
"room_temperature": "Температура воздуха в помещении",
"error_code": "Код ошибки кондиционера (0 — ошибок нет)",
"connection_state": "Состояние связи с модулем кондиционера",
"connectivity": "Есть ли связь с модулем",
"defrost": "Идёт разморозка наружного блока",
"maintenance": "Требуется обслуживание",
"oil_recovery": "Возврат масла в компрессор",
"pump_down": "Откачка хладагента (pump down)",
"check_operation": "Проверочный прогон",
"simultaneous": "Одновременные операции",
"different_modes": "Разморозка/масло/прочие режимы (шаблон B)",
},
"en": {
"climate": "Air conditioner control: mode, fan, setpoint, louvers, presets",
"room_temperature": "Room air temperature",
"error_code": "Air conditioner error code (0 — no errors)",
"connection_state": "Connection state with the AC module",
"connectivity": "Whether the module is reachable",
"defrost": "Outdoor unit defrost in progress",
"maintenance": "Maintenance required",
"oil_recovery": "Compressor oil recovery",
"pump_down": "Refrigerant pump down",
"check_operation": "Check operation",
"simultaneous": "Simultaneous operations",
"different_modes": "Defrost/oil/other modes (template B)",
},
}
def entity_description(language: str, key: str) -> str | None:
lang = "ru" if str(language).lower().startswith("ru") else "en"
return ENTITY_DESCRIPTIONS[lang].get(key) or FEATURE_DESCRIPTIONS[lang].get(
key
)
# op_status, биты (PROTOCOL §8.4): translation_key -> bit
OP_STATUS_BITS: dict[str, int] = {
"defrost": 24,