Skip to content

Поддержка вкл/выкл контура через hvac_mode + ряд фиксов (протестировано на H-1) - #54

Open
Anzic23 wants to merge 9 commits into
MihVS:mainfrom
Anzic23:upstream-pr
Open

Поддержка вкл/выкл контура через hvac_mode + ряд фиксов (протестировано на H-1)#54
Anzic23 wants to merge 9 commits into
MihVS:mainfrom
Anzic23:upstream-pr

Conversation

@Anzic23

@Anzic23 Anzic23 commented May 30, 2026

Copy link
Copy Markdown

Привет! Спасибо за интеграцию — пользуюсь на котле ZONT H-1.

В процессе настройки «нормального» термостата я сделал ряд правок в своём форке (Anzic23/zont_ha). Ветка лежит ровно поверх вашего main, конфликтов нет. Не настаиваю на полном слиянии — разбил всё по категориям, чтобы вы могли забрать только то, что считаете безопасным.

⚠️ Важно про тестирование. Всё проверялось только на H-1 (widget_type: zth, device_info.id = T100). На других моделях я проверить не могу. Поэтому ниже честно разделил «универсальные багфиксы» (на мой взгляд безопасны для всех) и «поведенческие изменения» (нужно ваше решение / тест на других приборах).


🟢 Категория 1. Универсальные багфиксы (предлагаю забрать в любом случае)

Это, как мне кажется, баги, проявляющиеся независимо от модели:

  1. Webhook отбрасывал события с пустым additional_info. AdditionalInfo.object_id был обязательным (str | int), а ZONT шлёт часть событий с additional_info: {}. Pydantic кидал ValidationError, событие падало в except ValueError → «Wrong webhook request», и HA обновлялся только по таймеру. Фикс: object_id: str | int | None = None. (core/models_zont_webhook.py)

  2. set_heating_mode слал неверное тело запроса. Отправлялось {'circuit_id': N}, а эндпоинт modes/{id}/actions/activate ожидает массив {'circuit_ids': [N]}. На H-1 без этого приходил not implemented. (core/zont.py)

  3. set_heating_mode_all_circuits слал запрос без тела. ZONT отвечал «Не указан Content-Type». Добавил json={}. (core/zont.py)


🟡 Категория 2. Улучшения (полезны всем, но это вопрос вкуса/нагрузки)

  1. Двухстадийный resync после webhook-события. target_temp в read-API обновляется с задержкой ~10с после смены режима (eventual consistency), поэтому первый async_request_refresh() ловит старую уставку. Добавил добор-опросы через 6 и 8 секунд. (__init__.py)

  2. Диапазон 5–80°C для отопительных контуров. Добавил MATCHES_HEATING = ('отопл', 'теплонос') по аналогии с GVS/FLOOR — иначе у контура теплоносителя брались значения из API, неудобные для climate-сущности. Диапазон выбран эмпирически. (const.py)

  3. TIME_UPDATE 60 → 30с. Это лично моя настройка (подстраховка для изменений без webhook). Для upstream — на ваше усмотрение, удваивает частоту опроса API.


🔴 Категория 3. Поведенческое изменение под H-1 (нужно ваше решение)

  1. Вкл/выкл контура через hvac_mode (HEAT/OFF). Основная правка (climate.py, ~155 строк). Сейчас в README прямо написано, что set_hvac_mode не поддерживается — «такова особенность ZONT». На H-1 мне удалось это обойти:

    • hvac_mode и preset_mode выводятся из per-circuit target_temp (<= 5° = OFF), а не из device-wide current_mode. На H-1 current_mode общий для всех контуров, и если выводить состояние из него — смена режима одного контура «дёргала» отображение остальных.
    • async_set_hvac_mode(HEAT/OFF) для отопления только активирует режим (Комфорт/Выключен) — активация сама ставит уставку и помечает режим активным в ЛК. Ручная установка target сбрасывала current_mode → None.
    • Для ГВС (нет режимов) вкл/выкл идёт через уставку (5° / 50°).
    • Контур ГВС при выключении в ЛК полностью исчезает из ответа API — добавил стабильный circuit_id + available, чтобы не падало с AttributeError, и сущность уходила в unavailable.
    • Роутинг команд режима по widget_type (z3k → V1, иначе → V3) и device_info.id ∈ {T100, T102} — у меня оригинальная логика по имени модели не срабатывала для H-1.

    Это меняет задокументированное поведение, и я не уверен, что логика target_temp ≤ 5 = OFF и device-wide режимы верны для всех приборов. Поэтому выношу отдельно — возможно, имеет смысл прятать за условие по модели/типу, либо обсудить.


Готов разбить на отдельные PR'ы (например, отдельно категорию 1), переписать под ваши соглашения или просто оставить как референс. Как вам удобнее?

Anzic23 added 9 commits May 30, 2026 00:06
Replace SetHvacModeError with async_set_hvac_mode:
- OFF maps to first mode with выкл/откл/off in name
- HEAT restores last known mode or picks first non-off mode
- Extract _async_apply_heating_mode shared by preset and hvac-mode setters
- Track _last_heat_mode_id in coordinator updates
…hvac_mode

Root cause of the 400 not_z3k_device error: heating-mode dispatch
guessed the API family from the model name (+/pro -> V3, else -> V1).
H-1 (widget_type=zth) fell into the V1 z3k branch and ZONT rejected it.

- Dispatch on device_info.widget_type: z3k -> API V1, otherwise V3.
- hvac_mode now reports OFF when the active heating mode is an off-mode
  (is_off alone does not reflect the Выключен mode).
- Track last non-off mode via _update_last_heat_mode for HEAT restore.
- Add TURN_ON/TURN_OFF features (HA 2026.x) so the power toggle maps
  to async_set_hvac_mode.
… 5-80

Per real H-1 behaviour (verified against live ZONT API):
- mode activation endpoint is device-wide; circuit_ids is effectively
  ignored, but ГВС works on_request so only Отопление target changes.
- thermostat OFF  -> set consumer target to HVAC_OFF_TEMP (5) + activate
  «Выключен»; HEAT -> set target to HVAC_HEAT_TEMP (40) + activate «Комфорт».
- hvac_mode derived from target_temp (<=5 = OFF): mode fields
  (current_mode/applied) are unreliable/desynced on H-1.
- Отопление temp limits 5-80 (MATCHES_HEATING); API min/max -30/100 is
  rejected by the validator and fell back to 5-35.

API fixes in core/zont.py:
- set_heating_mode_all_circuits: send json={} (без тела -> «Не указан
  Content-Type»); this also makes the heating-mode buttons work.
- set_heating_mode: body circuit_ids (array), not circuit_id (singular),
  which returned «not implemented».
- _async_apply_heating_mode routes T100/T102 by device_info.id too
  (H-1 reports device_info.id=T100).
Manual target-temp setting puts the circuit into manual mode
(current_mode -> None) and drops it from modes.applied, so the mode never
showed active in ZONT LK/app when toggled from HA. Verified on live API:
activating a mode alone auto-sets the heat-carrier target (Komfort->40,
Vykluchen->5) AND marks the mode applied across circuits.

- async_set_hvac_mode now just activates the mode; target follows.
- Makes HA and LK mode state consistent both directions
  (preset_mode reflects current_mode; LK shows the activated mode).
- Drop unused HVAC_HEAT_TEMP.

Note: GVS target is untouched by mode changes (works on_request).
ZONT pushes events (e.g. SelectHeatingMode) with additional_info={} but
AdditionalInfo.object_id was required -> pydantic ValidationError (a
ValueError) -> handler logged "Wrong webhook request" and dropped the
event, so LK/app changes never triggered an immediate HA refresh and
relied on the 60s poll instead.

Make object_id optional; webhook now parses and calls
coordinator.async_request_refresh() -> near-instant LK -> HA sync.
hvac_mode was read from target_temp while preset_mode read current_mode.
On mode switch ZONT updates current_mode and target_temp non-atomically,
so the thermostat could show hvac=off together with preset=Комфорт.
Derive hvac_mode from the active heating mode (off-mode -> OFF, else
HEAT); fall back to target_temp only when no mode is set (manual).
Root cause of "change one, another updates": heating modes on H-1 are
device-wide, so current_mode is identical on ГВС and Отопление. Deriving
hvac_mode/preset_mode from current_mode made toggling heating also flip
the DHW tile.

- hvac_mode and preset_mode now both derive from the per-circuit
  target_temp (<= HVAC_OFF_TEMP = OFF). Circuits are independent and the
  two properties never disagree. ГВС stays HEAT regardless of heating.
- webhook: two-stage delayed resync (6s, +8s) to cover ZONT read-API
  eventual-consistency lag (~10s) after a mode change.
- poll interval 60s -> 30s as a safety net for changes that emit no
  webhook (e.g. target-temp edits in the ZONT app).
Turning hot water off in ZONT LK removes the DHW circuit from the API
response entirely, not is_off=true. The climate entity then crashed on
every coordinator update: get_circuit returned None then None.id.

- Track a stable self._circuit_id; on update, if the circuit is missing,
  mark the entity unavailable instead of crashing. GVS shows unavailable
  while hot water is off and recovers when it is back on.
- DHW on/off cannot be driven via the widget v3 API; for the DHW
  thermostat hvac OFF/HEAT now sets the circuit target (5 / DHW_ON_TEMP)
  per-circuit instead of activating the device-wide mode.
@MihVS

MihVS commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Здравствуйте! Благодарю за вклад в развитие проекта! Извиняюсь за долгий ответ, не хватало времени. Предлагаю оставить только 1 и 2 категорию правок, а третью пока не добавлять. В начале сезона надо будет потестировать как будет работать на разных устройствах.

@MihVS

MihVS commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Если что, то можете написать мне в телеграм mihvs, для более быстрой обратной связи

@MihVS

MihVS commented Aug 7, 2026

Copy link
Copy Markdown
Owner

1 категория

  1. Согласен
  2. По доке нет списка, это видимо для вашей версии плк. Но для Н-1 предусмотрен другой запрос в интеграции.
  3. Поправил

2 категория

  1. Мысль понятна, поправил на своё усмотрение.
  2. Добавил, но MATCHES_HEATING = ('теплонос', )
  3. тут оставлю 60

3 категория

потестирую вашу идею на разных устройствах, ближе к сезону.

Просьба, подготовьте пул реквест только по 3 категории.
спасибо.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants