Qubino Flush Pilot (ZMNHJD): soporte para radiador de cable piloto

@Sescandell, estoy haciendo un PR para el hilo piloto para la integración del termostato y mi dispositivo QUBINO solo devuelve un variador y, en función del valor, corresponde a un estado del hilo piloto.

¿Hay alguna manera de tener modos en lugar de un variador en la integración? Te lo agradezco de antemano.

A continuación, un análisis de Claude:

# Qubino Flush Pilot (ZMNHJD): soporte para radiador de cable piloto

> Especificaciones de la integración externa Z-Wave. Se basa en el marco de las
> integraciones externas del monorepo Gladys (`docs/specs/external-integrations/`):
> los dispositivos se publican mediante `POST /discovered_device` (C.3, SDK
> `publishDiscoveredDevices`), los comandos llegan mediante
> `external-integration.device.set-value` (C.4), y los estados se envían mediante
> `POST /state` (B.6).

## 1. Problema

El Qubino Flush Pilot (ZMNHJD) controla un radiador de cable piloto. Lo hace a través de
su **Multilevel Switch** (CC 38): cada rango de niveles corresponde a una
orden de cable piloto. Actualmente, la integración lo publica como lo indica su
clase de dispositivo, es decir, un variador (posición 0–99 %, estado encendido/apagado,
« restaurar el valor anterior »).

Consecuencias:

- el usuario ve « 30 % » cuando el radiador está en **Eco**;
- el termostato no puede controlarlo. Su actuador de cable piloto
  (`THERMOSTAT_PILOT_WIRE_FEATURE`, especificación del termostato C.1.1) solo acepta una
  funcionalidad `heater` / `pilot-wire-mode`;
- las escenas, el panel y HomeKit ven un variador, no una orden.

La traducción nivel → orden es específica de este producto: por lo tanto, pertenece a
la integración que conoce este producto. El termostato, como el resto de Gladys,
no ve más que el tipo estándar.

## 2. Identificar el dispositivo

| Campo | Valor |
|---|---|
| `manufacturerId` | 345 (`0x0159`, Qubino) |
| `productType` | 4 (`0x0004`) |
| `productId` | 81 (`0x0051`) |
| `deviceId` zwave-js | `345-81-4` |
| `deviceClass` | básico 4, genérico 17, específico 1 |
| configuración zwave-js | `0x0159/zmnhjd.json`, etiqueta `ZMNHJD`, « Flush Pilot » |

**La clase de dispositivo no es utilizable.** Genérico 17 / específico 1
(« Multilevel Switch, variador ») es la clase de todos los variadores. Usarla
transformaría cada variador en un cable piloto. El dispositivo se identifica por su
**identificador de producto** (`manufacturerId` + `productType` + `productId`), verificado
**antes** de cualquier regla basada en la clase de dispositivo.

## 3. Dispositivo publicado

El nodo publica **una sola** funcionalidad accionable en lugar de las del
variador:

| Campo | Valor |
|---|---|
| `category` | `heater` (`DEVICE_FEATURE_CATEGORIES.HEATER`) |
| `type` | `pilot-wire-mode` (`DEVICE_FEATURE_TYPES.HEATER.PILOT_WIRE_MODE`) |
| `min` / `max` | `0` / `5` (`PILOT_WIRE_MODE.OFF` … `PILOT_WIRE_MODE.COMFORT`) |
| `read_only` | `false` |
| `has_feedback` | `true`: el módulo devuelve `currentValue` después de cada cambio, incluido un
presionar sus propios botones |
| `keep_history` | `true` |
| valor fuente | CC 38 `currentValue`, endpoint 0 |

**No publicados** para este producto:

- las funcionalidades del variador (`position`, el `estado` encendido/apagado deducido del
  nivel, `restorePrevious`). « Restaurar el valor anterior » enviaría al radiador una
  orden arbitraria;
- el Binary Switch explícito (CC 37). La integración ya lo elimina de cualquier nodo
  que tenga un Multilevel Switch;
- `Up` / `Down` / `duration` / `event` (CC 38), como hoy en todos los
  nodos.

Inalterados: los parámetros de configuración (CC 112: tipos de entrada, modo de las
entradas 11/12/13, estado después de un corte de energía 30). Permanecen fuera del
ámbito (sección 8).

## 4. Escribir una orden (Gladys → dispositivo)

En `external-integration.device.set-value` para esta funcionalidad, `value`
es un `PILOT_WIRE_MODE`. Se escribe en forma de nivel por el comando
`set` del Multilevel Switch (CC 38, endpoint 0):

| Orden (`PILOT_WIRE_MODE`) | Valor | Nivel escrito |
|---|---|---|
| `OFF` (apagado) | 0 | 0 |
| `FROST_PROTECTION` (protección contra heladas) | 1 | 20 |
| `ECO` | 2 | 30 |
| `COMFORT_2` (confort −2 °C) | 4 | 40 |
| `COMFORT_1` (confort −1 °C) | 3 | 50 |
| `COMFORT` (confort) | 5 | 99 |

- Los niveles escritos son los medidos en el módulo: 0 / 20 / 30 / 40 / 50 / 99.
- Cualquier otro valor recibe un **command-result en error** (« orden de cable piloto
  desconocida »), y nada se envía al módulo.
- **Sin estado optimista.** El estado se envía cuando el módulo confirma con
  `currentValue` (sección 5), no al enviar el comando. Un radiador que ha fallado en
  la trama no debe parecer que ha obedecido.

## 5. Leer una orden (dispositivo → Gladys)

Cada actualización de `currentValue` en la CC 38, el nivel se lee **por
rango**, y la orden se envía mediante `POST /state`:

| Nivel | Orden enviada |
|---|---|
| 0–10 | `OFF` |
| 11–20 | `FROST_PROTECTION` |
| 21–30 | `ECO` |
| 31–40 | `COMFORT_2` |
| 41–50 | `COMFORT_1` |
| 51–99 | `COMFORT` |
| otro (por ejemplo, 255, un valor no numérico) | nada se envía |

La lectura por rango en lugar de por valor exacto es indispensable: el módulo
puede devolver cualquier nivel de un rango. Se ha observado un clic en su botón en
**60** antes de 99, y es una orden de confort.

`targetValue` no se lee: `currentValue` es lo que el módulo aplica
realmente.

## 6. Dispositivos creados antes de este cambio

Un nodo ya creado en Gladys como variador mantiene sus funcionalidades de
variador hasta que no se actualiza desde la pantalla de descubrimiento. La actualización
las reemplaza por la funcionalidad de cable piloto: mismo `external_id` de
dispositivo, nuevo `external_id` de funcionalidad.

- El historial de las antiguas funcionalidades de variador no se recupera. Los
  niveles y las órdenes no son los mismos valores.
- Las escenas y los widgets del panel que apuntaban a la antigua
  funcionalidad de variador deben volver a apuntar manualmente. La migración de
  dispositivo (`device-migration.md`) mueve los selectores de una funcionalidad a
  otra, pero un « 30 % » en una escena no significaría una orden.
- Un termostato (virtual, `THERMOSTAT_PILOT_WIRE_FEATURE`) se configura luego
  en la nueva funcionalidad en su formulario de edición. Nada más en el lado del
  termostato: la correspondencia preset → orden está en la especificación del
  termostato, C.1.1.

## 7. Pruebas

- **Descubrimiento:**
  - un nodo de `deviceId` `345-81-4` y clase 17-1 publica exactamente una
    funcionalidad `heater` / `pilot-wire-mode`, sin posición, estado,
    `restorePrevious` ni Binary Switch;
  - un nodo de clase 17-1 con **otro** identificador de producto sigue
    publicándose como un variador (no regresión).
- **Escritura:** cada una de las seis órdenes produce un `set` CC 38 con su nivel, y
  una orden desconocida da un command-result en error, sin enviar nada.
- **Lectura:** los límites de los rangos (0, 10, 11, 20, 21, 30, 31, 40, 41, 50, 51,
  99), el 60 observado → `COMFORT`, y 255 / −1 / un valor no numérico no
  envían nada.
- **Retorno de estado:** después de una escritura, ningún estado se envía hasta que
  `currentValue` no haya llegado.

## 8. Fuera del alcance

- Los parámetros de configuración CC 112 (modos de los botones, estado después de un
  corte de energía).
- Otras referencias de Qubino al cable piloto, mientras no se conozca su identificador de
  producto y no se confirme que usan los mismos niveles. Cada una se añade
  explícitamente a la lista de productos, nunca por la clase de dispositivo.
- Cualquier lógica de regulación: la integración traduce las órdenes, es el
  termostato el que las decide.

## 9. Verificación manual

1. Actualizar el nodo « Radiador (Oficina) » desde la pantalla de descubrimiento: aparece
   una sola funcionalidad « cable piloto ».
2. Desde el dispositivo, enviar cada orden y verificar el nivel en
   zwave-js-ui: apagado 0, protección contra heladas 20, eco 30, confort −2 40, confort −1 50,
   confort 99.
3. Presionar el botón del módulo: la orden mostrada en Gladys sigue (60 →
   confort).
4. En el formulario del termostato, elegir esta funcionalidad como actuador de cable piloto, luego elegir Eco en el widget: el módulo pasa a 30.

```## 10. Puntos abiertos

- **Bornes de las playas.** Las escrituras (0/20/30/40/50/99) están medidas. Los
  rangos de lectura provienen de la documentación de Qubino y aún deben verificarse
  en la documentación del ZMNHJD.
- **Numeración confort −1 / −2.** `PILOT_WIRE_MODE` devuelve `COMFORT_1 = 3` y
  `COMFORT_2 = 4`. La tabla de la sección 4 sigue la constante, no el orden de los
  niveles (40 = confort −2, 50 = confort −1).

Con este módulo y la integración zwavejs, tenemos los estados/órdenes en la interfaz de usuario (tenía 2) y debe enviar los valores del variador para el comando a continuación.
¿No se puede implementar este cambio en el termostato del futuro?
¿O el termostato solo recupera el estado/orden y no el valor del variador?