Files
fgl-aircon/custom_components/fglair/const.py
T
petr.polezhaev 976a2dcbb8 ha(ux): исправления по ревью — быстрый выход пробинга, prefill шаблона, ниты
- major: probe_template прекращает перебор на unreachable/no_slots/key_error
  (недоступный модуль: ~10 c вместо ~49 c), продолжает только при
  properties_missing (доступен, но чужой шаблон); +тест
- m1: extra_state_attributes не падает до добавления в hass
- m2: reconfigure берёт сохранённый шаблон как дефолт (не пересчитывает по
  модели); +тест
- m3: device_capabilities/display_temperature переведены в необязательные
  зонды (trial не падает на молчащем caps; feature-модель трактует None
  как «не режем»)
- m4: async_sync_now(online_timeout) — честное имя/докстринг
- ниты: удалены PRESET_CAP/as_dict/лишние плейсхолдеры, DIRECTION_KEYS
  упрощён, тавтология в тесте исправлена, en-тире
- 59 components + 47 pyfglair/acceptance зелёные
2026-09-29 18:02:52 +03:00

259 lines
11 KiB
Python

"""Константы и словари значений интеграции fglair (PROTOCOL §8)."""
from __future__ import annotations
from homeassistant.components.climate import HVACMode
from pyfglair import Prop, State, Template
from pyfglair.provision import REGIONS as _PROVISION_REGIONS
DOMAIN = "fglair"
CONF_HOST = "host"
CONF_DEVICE_PORT = "device_port"
CONF_DSN = "dsn"
CONF_LANIP_KEY = "lanip_key"
CONF_LANIP_KEY_ID = "lanip_key_id"
CONF_MODEL = "model"
CONF_TEMPLATE = "template"
CONF_NAME = "name"
CONF_LISTEN_PORT = "listen_port"
CONF_KEEPALIVE_MS = "keepalive_ms"
CONF_REGION = "region"
CONF_EMAIL = "email"
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"
CONF_TEMP_DEN = "temp_den"
CONF_TEMP_OFFSET = "temp_offset"
DEFAULT_DEVICE_PORT = 80
# 0 => свободный эфемерный порт: несколько ConfigEntry (несколько
# кондиционеров) не конфликтуют; модулю фактический порт сообщается в
# local_reg (PROTOCOL §1).
DEFAULT_LISTEN_PORT = 0
DEFAULT_KEEPALIVE_MS = 15000
TRIAL_TIMEOUT = 10.0
REGIONS = sorted(_PROVISION_REGIONS)
TRIAL_ERRORS = {
"no_slots": "no_slots",
"key_mismatch": "key_mismatch",
"unreachable": "cannot_connect",
"timeout": "timeout",
"start_failed": "cannot_connect",
"properties_missing": "properties_missing",
}
# ---------------------------------------------------------------------------
# operation_mode (PROTOCOL §8.3): 0=OFF, 1=ON, 2=AUTO, 3=COOL, 4=DRY,
# 5=FAN, 6=HEAT
# ---------------------------------------------------------------------------
OPERATION_MODE_OFF = 0
OPERATION_MODE_ON = 1
HVAC_MODE_TO_VALUE = {
HVACMode.AUTO: 2,
HVACMode.COOL: 3,
HVACMode.DRY: 4,
HVACMode.FAN_ONLY: 5,
HVACMode.HEAT: 6,
}
HVAC_VALUE_TO_MODE = {value: mode for mode, value in HVAC_MODE_TO_VALUE.items()}
# device_capabilities, биты (PROTOCOL §8.5)
CAP_HVAC = {
HVACMode.AUTO: 4,
HVACMode.COOL: 0,
HVACMode.DRY: 1,
HVACMode.FAN_ONLY: 2,
HVACMode.HEAT: 3,
}
CAP_FAN = {"auto": 5, "high": 6, "medium": 7, "low": 8, "quiet": 9}
CAP_SWING_VERTICAL = 10
CAP_SWING_HORIZONTAL = 11
CAP_ECONOMY = 12
CAP_MIN_HEAT = 13
CAP_INDOOR_FAN_CONTROL = 14
CAP_POWERFUL = 16
CAP_OUTDOOR_LOW_NOISE = 17
CAP_COIL_DRY = 18
# fan_speed (PROTOCOL §8.3)
FAN_VALUE_TO_MODE = {0: "quiet", 1: "low", 2: "medium", 3: "high", 4: "auto"}
FAN_MODE_TO_VALUE = {mode: value for value, mode in FAN_VALUE_TO_MODE.items()}
# preset: ECO = economy_mode, BOOST = powerful_mode
PRESET_ECO = "eco"
PRESET_BOOST = "boost"
PRESET_PROP = {
PRESET_ECO: Prop.ECONOMY_MODE,
PRESET_BOOST: Prop.POWERFUL_MODE,
}
SWING_MODES = ["off", "on"]
# switch-свойства: prop -> (translation_key, capability bit | None)
SWITCH_PROPS: dict[Prop, tuple[str, int | None]] = {
Prop.ECONOMY_MODE: ("economy_mode", CAP_ECONOMY),
Prop.POWERFUL_MODE: ("powerful_mode", CAP_POWERFUL),
Prop.COIL_DRY_MODE: ("coil_dry_mode", CAP_COIL_DRY),
Prop.MIN_HEAT: ("min_heat", CAP_MIN_HEAT),
Prop.OUTDOOR_LOW_NOISE: ("outdoor_low_noise", CAP_OUTDOOR_LOW_NOISE),
Prop.HUMAN_DET_AUTO_SAVE: ("human_det_auto_save", None),
Prop.WIFI_LED_ENABLE: ("wifi_led_enable", 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,
"maintenance": 22,
"oil_recovery": 28,
"pump_down": 29,
"check_operation": 30,
"simultaneous": 25,
"different_modes": 21,
}
# Уставка: фактические ограничения бытового прибора (PROTOCOL §8.3 [LEGACY]);
# единицы API = 0.1 °C.
TEMP_MIN_TENTHS = 160
TEMP_MAX_TENTHS = 300
TEMP_STEP_TENTHS = {
Template.A: 5,
Template.B: 10,
Template.F: 5,
}
# Состояния, в которых сущности доступны (PROTOCOL/план §5).
AVAILABLE_STATES = (State.ONLINE, State.RECOVERING)