diff --git a/docs/CBUS_PROTOCOL.md b/docs/CBUS_PROTOCOL.md index ef8d5365..a95d17fc 100644 --- a/docs/CBUS_PROTOCOL.md +++ b/docs/CBUS_PROTOCOL.md @@ -77,6 +77,9 @@ Controls motorized devices like blinds, curtains, and garage doors. - `OFF ` - Start closing/lowering (fully closed) - `RAMP ` - Set position level (0-255, where 0=closed, 255=open) - `TERMINATERAMP ` - Stop movement at current position +- `ENABLE SET //{project}/{net}/{app}/{group} <0-255>` - Set the native Enable Control byte (MQTT `.../set`; opt-in via `cbus_enable_control_app_id`) +- `ENABLE LABEL //{project}/{net}/{app} hex ` - Set the group label (MQTT `.../label`; UTF-8 hex, language 1) +- `ENABLE REMOVE //{project}/{net}/{app}/{group}` - Delete the C-Gate group object (MQTT `.../remove` with payload `ON`) **Events:** - `enable on //` - Cover fully opened diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index 35eba930..754134e0 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -199,7 +199,7 @@ The poll expands to `network/application` pairs covering lighting (56) plus whic | `ha_discovery_networks` | `ha_discovery_networks` | number[] | `[]` | Networks to scan for discovery. In the add-on this falls back to `getall_networks` when left empty. Also drives the security zone status sync. | | `ha_discovery_scene_enabled` | `ha_discovery_scene_enabled` | boolean | `true` | Publish an HA `scene` entity per trigger group alongside the `event` and `button` entities. Set `false` to suppress scenes. | -> **Add-on gating:** most `ha_discovery_*` options are only applied when `ha_discovery_enabled` is on. The deliberate exceptions are `cbus_aircon_app_id`, `cbus_security_app_id`, the three `cbus_security_*_enabled` switches, `cbus_measurement_app_id`, `cbus_clock_enabled` and `cbus_scene_module_enabled` — those publish over plain MQTT and would otherwise silently stop working for MQTT-only installs. +> **Add-on gating:** most `ha_discovery_*` options are only applied when `ha_discovery_enabled` is on. The deliberate exceptions are `cbus_aircon_app_id`, `cbus_security_app_id`, the three `cbus_security_*_enabled` switches, `cbus_measurement_app_id`, `cbus_clock_enabled`, `cbus_scene_module_enabled` and `cbus_enable_control_app_id` — those publish over plain MQTT and would otherwise silently stop working for MQTT-only installs. --- @@ -297,6 +297,14 @@ The sensors carry no `device_class`. Home Assistant's `timestamp` class wants IS |---|---|---|---|---| | `cbus_scene_module_enabled` | `cbus_scene_module_enabled` | boolean | `false` | Accept MQTT `cbus/write/{net}/{app}/{set}/play` and `/record` payloads (scene number 0–255) that become C-Gate `SCENE PLAY` / `SCENE RECORD`. Off by default: most installs use Trigger Control scenes instead, and a record write overwrites module memory. | +## Enable Control extra verbs + +ON/OFF/RAMP on Enable Control already work through the generic lighting-style topics. These extra C-Gate verbs are MQTT-only (no Home Assistant entities). + +| Setting | Add-on option | Type | Default | Notes | +|---|---|---|---|---| +| `cbus_enable_control_app_id` | `cbus_enable_control_app_id` | string \| null | `null` | Typically `203` (`$CB`). Gates `cbus/write/{net}/{app}/{group}/set` (native byte 0–255, or ON/OFF), `/label` (UTF-8 text, hex-encoded, clipped to 32 characters, English language code 1), and `/remove` (payload must be ON; this **deletes the C-Gate group object**). Leave unset to ignore those topics. | + Temperature Broadcast (app 25) reads are always decoded. Writes use `cbus/write/{net}/25/{group}/temperature` with a Celsius payload (0.0–63.75); cgateweb sends `TEMPERATURE BROADCAST` with `rawByte = round(°C × 4)`. --- @@ -508,4 +516,4 @@ The one setting that is read but has no effect is `logging` — see [Logging](#l 7. **`web_port` is standalone-only, by design.** The add-on hardcodes `ingress_port: 8080` and its watchdog to the same port, so exposing it would let a user break ingress and the Supervisor watchdog at once. 8. **`mqttCertFile` and `mqttKeyFile` are standalone-only.** The add-on exposes `mqtt_ca_file` but no client certificate or key option, so mutual TLS is not configurable there. 9. **`trace` is standalone-only.** The add-on's `log_level` schema is `list(debug|info|warn|error)`; anything else falls back to `info`. -10. **Most `ha_discovery_*` add-on options are ignored when `ha_discovery_enabled` is off.** The exceptions — `cbus_aircon_app_id`, `cbus_security_app_id`, the `cbus_security_*_enabled` switches, `cbus_measurement_app_id`, `cbus_clock_enabled` and `cbus_scene_module_enabled` — are deliberate, so MQTT-only installs keep working. +10. **Most `ha_discovery_*` add-on options are ignored when `ha_discovery_enabled` is off.** The exceptions — `cbus_aircon_app_id`, `cbus_security_app_id`, the `cbus_security_*_enabled` switches, `cbus_measurement_app_id`, `cbus_clock_enabled`, `cbus_scene_module_enabled` and `cbus_enable_control_app_id` — are deliberate, so MQTT-only installs keep working. diff --git a/homeassistant-addon/CHANGELOG.md b/homeassistant-addon/CHANGELOG.md index d91bf64e..1b23c7aa 100644 --- a/homeassistant-addon/CHANGELOG.md +++ b/homeassistant-addon/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 If this add-on saves you time, you can [buy me a coffee](https://buymeacoffee.com/dougrathbone). +## [1.32.0] - 2026-08-25 + +### Added + +- **Enable Control groups can be set, labelled, and removed over MQTT.** Opt in with the Enable Control application ID (typically 203); remove deletes the C-Gate group and needs ON on the remove topic. + ## [1.31.0] - 2026-08-25 ### Added diff --git a/homeassistant-addon/DOCS.md b/homeassistant-addon/DOCS.md index 85f9eef9..b47bb566 100644 --- a/homeassistant-addon/DOCS.md +++ b/homeassistant-addon/DOCS.md @@ -393,6 +393,7 @@ Disable auto-discovery (`auto_discover_networks: false`) if: | `cbus_measurement_app_id` | integer | (null) | C-Bus Measurement application id (`228`) for analogue/numeric sensor readings (temperature, power, light level, energy, etc). Decodes `measurement data ...` events and publishes `cbus/read/{network}/228/{device}/{channel}/value` (the decoded number) and `/unit` (e.g. `W`, `°C`, `lx`; empty if unitless/custom), with a `sensor` entity auto-created per device/channel. Also gates the write path: publish `value,multiplier,units` to `cbus/write/{network}/228/{device}/{channel}/data` to inject a reading onto C-Bus (e.g. from a scripted/virtual sensor) — cgateweb sends the native `MEASUREMENT DATA` command. One setting gates both directions, since — unlike Air Conditioning/Security — this isn't a hardware-control write; it's how a measurement source (physical or scripted) gets its own data onto the bus. Off by default. | | `cbus_clock_enabled` | boolean | `false` | Decode C-Bus Clock and Timekeeping (app 223) broadcasts as two diagnostic sensors on the network device, and ask C-Gate for a clock refresh on connect. Read-only: this never sets the network clock. Off by default. | | `cbus_scene_module_enabled` | boolean | `false` | Accept MQTT `cbus/write/{network}/{app}/{set}/play` and `/record` (payload is the scene number) for a C-Bus Scene Module. Off by default: most installs use Trigger Control scenes, and a record write overwrites module memory. | +| `cbus_enable_control_app_id` | integer | (null) | C-Bus Enable Control application id (typically `203`) for extra MQTT writes: `cbus/write/{network}/{app}/{group}/set` (native 0–255, or ON/OFF), `/label` (group name, hex-encoded), and `/remove` (payload ON; deletes the C-Gate group object). Leave empty to disable. No Home Assistant entities. ON/OFF/RAMP on that application still work without this. | | `ha_bridge_diagnostics_enabled` | boolean | `true` | Publish bridge health/diagnostic entities to Home Assistant via MQTT Discovery | | `ha_bridge_diagnostics_interval_sec` | integer | `60` | How often to refresh bridge diagnostic states (seconds) | diff --git a/homeassistant-addon/config.yaml b/homeassistant-addon/config.yaml index 94da2a68..f92aedec 100644 --- a/homeassistant-addon/config.yaml +++ b/homeassistant-addon/config.yaml @@ -1,5 +1,5 @@ name: "C-Gate Web Bridge" -version: "1.31.0" +version: "1.32.0" slug: cgateweb description: "Bridge between Clipsal C-Bus systems and MQTT/Home Assistant" url: "https://github.com/dougrathbone/cgateweb" @@ -199,6 +199,9 @@ schema: cbus_measurement_app_id: "int(1,255)?" cbus_clock_enabled: "bool?" cbus_scene_module_enabled: "bool?" + # Enable Control extra verbs (typically 203/$CB). Optional; omit from + # options. SET/LABEL/REMOVE MQTT writes only — no HA entities. + cbus_enable_control_app_id: "int(1,255)?" ha_hvac_temperature_unit: "list(C|F)?" # ── Bridge Diagnostics ── diff --git a/homeassistant-addon/translations/catalog.yaml b/homeassistant-addon/translations/catalog.yaml index a8d6fef6..4a3bfc51 100644 --- a/homeassistant-addon/translations/catalog.yaml +++ b/homeassistant-addon/translations/catalog.yaml @@ -4445,6 +4445,104 @@ configuration: Типово вимкнено: більшість використовує сцени Trigger Control, а запис перезаписує пам'ять модуля." + cbus_enable_control_app_id: + name: + en: Enable Control Application ID + de: Enable-Control-Anwendungs-ID + es: ID de aplicación Enable Control + fr: ID d'application Enable Control + it: ID applicazione Enable Control + nl: Enable Control-toepassings-ID + pt: ID da aplicação Enable Control + ru: ID приложения Enable Control + zh: Enable Control 应用 ID + ja: Enable Control アプリケーションID + ko: Enable Control 애플리케이션 ID + pl: ID aplikacji Enable Control + sv: Enable Control-applikations-ID + no: Enable Control-applikasjons-ID + da: Enable Control-applikations-id + cs: ID aplikace Enable Control + uk: ID застосунку Enable Control + description: + en: >- + C-Bus Enable Control application id (typically 203) for MQTT set, label + and remove writes. Leave empty to disable. Remove deletes the C-Gate + group object — publish ON on the remove topic. Labels are hex-encoded. + No Home Assistant entities are created. + de: >- + C-Bus Enable-Control-Anwendungs-ID (typisch 203) für MQTT-Schreibvorgänge + set, label und remove. Leer lassen zum Deaktivieren. Remove löscht das + C-Gate-Gruppenobjekt — ON auf das Remove-Topic veröffentlichen. Labels + werden hex-kodiert. Es werden keine Home-Assistant-Entitäten erstellt. + es: >- + ID de aplicación Enable Control de C-Bus (típicamente 203) para escrituras + MQTT set, label y remove. Vacío para desactivar. Remove elimina el objeto + de grupo de C-Gate: publique ON en el topic remove. Las etiquetas se + codifican en hex. No se crean entidades de Home Assistant. + fr: >- + ID d'application Enable Control C-Bus (typiquement 203) pour les écritures + MQTT set, label et remove. Laisser vide pour désactiver. Remove supprime + l'objet groupe C-Gate — publiez ON sur le topic remove. Les libellés sont + encodés en hex. Aucune entité Home Assistant n'est créée. + it: >- + ID applicazione Enable Control C-Bus (in genere 203) per scritture MQTT + set, label e remove. Lasciare vuoto per disattivare. Remove elimina + l'oggetto gruppo C-Gate: pubblicare ON sul topic remove. Le etichette + sono in hex. Non vengono create entità Home Assistant. + nl: >- + C-Bus Enable Control-toepassings-ID (meestal 203) voor MQTT-writes set, + label en remove. Leeg laten om uit te schakelen. Remove verwijdert het + C-Gate-groepsobject — publiceer ON op het remove-topic. Labels zijn + hex-gecodeerd. Er worden geen Home Assistant-entiteiten aangemaakt. + pt: >- + ID da aplicação Enable Control C-Bus (tipicamente 203) para escritas MQTT + set, label e remove. Deixe vazio para desativar. Remove apaga o objeto de + grupo do C-Gate — publique ON no tópico remove. Etiquetas em hex. Não + cria entidades do Home Assistant. + ru: >- + ID приложения Enable Control C-Bus (обычно 203) для MQTT-записей set, + label и remove. Пусто — выкл. Remove удаляет объект группы C-Gate — + публикуйте ON в топик remove. Метки в hex. Сущности Home Assistant не + создаются. + zh: C-Bus Enable Control 应用 ID(通常为 203),用于 MQTT 的 set、label 和 remove 写入。留空即关闭。remove 会删除 C-Gate + 组对象——请在 remove 主题发布 ON。标签以十六进制编码。不会创建 Home Assistant 实体。 + ja: C-Bus Enable Control アプリケーションID(通常 203)。MQTT の set / label / remove 書き込み用。空で無効。remove は + C-Gate グループオブジェクトを削除します。remove トピックに ON を発行してください。ラベルは hex 符号化。Home + Assistant エンティティは作成しません。 + ko: "C-Bus Enable Control 애플리케이션 ID(보통 203). MQTT set, label, remove 쓰기용. 비우면 + 꺼짐. remove는 C-Gate 그룹 객체를 삭제하므로 remove 토픽에 ON을 게시하세요. 라벨은 hex 인코딩. + Home Assistant 엔티티는 만들지 않습니다." + pl: >- + ID aplikacji Enable Control C-Bus (zazwyczaj 203) dla zapisów MQTT set, + label i remove. Puste wyłącza. Remove usuwa obiekt grupy C-Gate — + publikuj ON na temacie remove. Etykiety w hex. Nie tworzy encji Home + Assistant. + sv: >- + C-Bus Enable Control-applikations-ID (vanligen 203) för MQTT-skrivningar + set, label och remove. Tomt inaktiverar. Remove tar bort C-Gate-gruppobjektet + — publicera ON på remove-ämnet. Etiketter hex-kodas. Inga Home + Assistant-entiteter skapas. + no: >- + C-Bus Enable Control-applikasjons-ID (vanligvis 203) for MQTT-skriving + set, label og remove. Tomt slår av. Remove sletter C-Gate-gruppeobjektet + — publiser ON på remove-emnet. Etiketter hex-kodes. Ingen Home + Assistant-entiteter opprettes. + da: >- + C-Bus Enable Control-applikations-id (typisk 203) til MQTT-skrivninger + set, label og remove. Tomt slår fra. Remove sletter C-Gate-gruppeobjektet + — publicér ON på remove-emnet. Etiketter hex-kodes. Der oprettes ingen + Home Assistant-entiteter. + cs: >- + ID aplikace Enable Control C-Bus (typicky 203) pro MQTT zápisy set, label + a remove. Prázdné vypne. Remove smaže objekt skupiny C-Gate — publikujte + ON na téma remove. Štítky v hex. Nevytváří entity Home Assistant. + uk: >- + ID застосунку Enable Control C-Bus (зазвичай 203) для MQTT-записів set, + label і remove. Порожнє вимикає. Remove видаляє об'єкт групи C-Gate — + публікуйте ON у топік remove. Мітки в hex. Сутності Home Assistant не + створюються. + ha_bridge_diagnostics_enabled: name: en: Enable Bridge Diagnostics diff --git a/homeassistant-addon/translations/cs.yaml b/homeassistant-addon/translations/cs.yaml index 7b9e4f7e..afb14de9 100644 --- a/homeassistant-addon/translations/cs.yaml +++ b/homeassistant-addon/translations/cs.yaml @@ -305,6 +305,12 @@ configuration: Přijímá MQTT příkazy pro přehrání nebo nahrání scén na C-Bus Scene Module. Ve výchozím stavu vypnuto: většina používá scény Trigger Control a nahrání přepíše paměť modulu. + cbus_enable_control_app_id: + name: ID aplikace Enable Control + description: >- + ID aplikace Enable Control C-Bus (typicky 203) pro MQTT zápisy set, label + a remove. Prázdné vypne. Remove smaže objekt skupiny C-Gate — publikujte + ON na téma remove. Štítky v hex. Nevytváří entity Home Assistant. ha_bridge_diagnostics_enabled: name: Povolit diagnostiku bridge description: >- diff --git a/homeassistant-addon/translations/da.yaml b/homeassistant-addon/translations/da.yaml index b99d408c..6e50bd96 100644 --- a/homeassistant-addon/translations/da.yaml +++ b/homeassistant-addon/translations/da.yaml @@ -309,6 +309,13 @@ configuration: Accepterer MQTT-kommandoer der afspiller eller optager scener på et C-Bus Scene Module. Fra som standard: de fleste bruger Trigger Control, og optagelse overskriver modulets hukommelse. + cbus_enable_control_app_id: + name: Enable Control-applikations-id + description: >- + C-Bus Enable Control-applikations-id (typisk 203) til MQTT-skrivninger + set, label og remove. Tomt slår fra. Remove sletter C-Gate-gruppeobjektet + — publicér ON på remove-emnet. Etiketter hex-kodes. Der oprettes ingen + Home Assistant-entiteter. ha_bridge_diagnostics_enabled: name: Aktivér bridge-diagnostik description: >- diff --git a/homeassistant-addon/translations/de.yaml b/homeassistant-addon/translations/de.yaml index 51276241..7baa64fb 100644 --- a/homeassistant-addon/translations/de.yaml +++ b/homeassistant-addon/translations/de.yaml @@ -321,6 +321,13 @@ configuration: Akzeptiert MQTT-Befehle zum Abspielen oder Aufzeichnen von Szenen auf einem C-Bus Scene Module. Standardmäßig aus: die meisten Anlagen nutzen Trigger-Control-Szenen, und Record überschreibt den Modulspeicher. + cbus_enable_control_app_id: + name: Enable-Control-Anwendungs-ID + description: >- + C-Bus Enable-Control-Anwendungs-ID (typisch 203) für MQTT-Schreibvorgänge + set, label und remove. Leer lassen zum Deaktivieren. Remove löscht das + C-Gate-Gruppenobjekt — ON auf das Remove-Topic veröffentlichen. Labels + werden hex-kodiert. Es werden keine Home-Assistant-Entitäten erstellt. ha_bridge_diagnostics_enabled: name: Bridge-Diagnose aktivieren description: >- diff --git a/homeassistant-addon/translations/en.yaml b/homeassistant-addon/translations/en.yaml index b6f661e8..1c4f8074 100644 --- a/homeassistant-addon/translations/en.yaml +++ b/homeassistant-addon/translations/en.yaml @@ -301,6 +301,13 @@ configuration: Accept MQTT commands that play or record scenes on a C-Bus Scene Module. Off by default: most installs use Trigger Control scenes instead, and a record write overwrites module memory. + cbus_enable_control_app_id: + name: Enable Control Application ID + description: >- + C-Bus Enable Control application id (typically 203) for MQTT set, label + and remove writes. Leave empty to disable. Remove deletes the C-Gate group + object — publish ON on the remove topic. Labels are hex-encoded. No Home + Assistant entities are created. ha_bridge_diagnostics_enabled: name: Enable Bridge Diagnostics description: >- diff --git a/homeassistant-addon/translations/es.yaml b/homeassistant-addon/translations/es.yaml index 42e7210a..4749c25a 100644 --- a/homeassistant-addon/translations/es.yaml +++ b/homeassistant-addon/translations/es.yaml @@ -324,6 +324,13 @@ configuration: Acepta comandos MQTT para reproducir o grabar escenas en un Scene Module C-Bus. Desactivado por defecto: la mayoría usa escenas de Trigger Control, y grabar sobrescribe la memoria del módulo. + cbus_enable_control_app_id: + name: ID de aplicación Enable Control + description: >- + ID de aplicación Enable Control de C-Bus (típicamente 203) para escrituras + MQTT set, label y remove. Vacío para desactivar. Remove elimina el objeto + de grupo de C-Gate: publique ON en el topic remove. Las etiquetas se + codifican en hex. No se crean entidades de Home Assistant. ha_bridge_diagnostics_enabled: name: Habilitar diagnósticos del bridge description: >- diff --git a/homeassistant-addon/translations/fr.yaml b/homeassistant-addon/translations/fr.yaml index 697936b2..908b1886 100644 --- a/homeassistant-addon/translations/fr.yaml +++ b/homeassistant-addon/translations/fr.yaml @@ -329,6 +329,13 @@ configuration: Scene Module C-Bus. Désactivé par défaut : la plupart des installs utilisent Trigger Control, et un enregistrement écrase la mémoire du module. + cbus_enable_control_app_id: + name: ID d'application Enable Control + description: >- + ID d'application Enable Control C-Bus (typiquement 203) pour les écritures + MQTT set, label et remove. Laisser vide pour désactiver. Remove supprime + l'objet groupe C-Gate — publiez ON sur le topic remove. Les libellés sont + encodés en hex. Aucune entité Home Assistant n'est créée. ha_bridge_diagnostics_enabled: name: Activer les diagnostics du bridge description: >- diff --git a/homeassistant-addon/translations/it.yaml b/homeassistant-addon/translations/it.yaml index 9e2a4369..d8bad99c 100644 --- a/homeassistant-addon/translations/it.yaml +++ b/homeassistant-addon/translations/it.yaml @@ -324,6 +324,13 @@ configuration: Accetta comandi MQTT per riprodurre o registrare scene su un Scene Module C-Bus. Disattivo di default: la maggior parte usa scene Trigger Control, e la registrazione sovrascrive la memoria del modulo. + cbus_enable_control_app_id: + name: ID applicazione Enable Control + description: >- + ID applicazione Enable Control C-Bus (in genere 203) per scritture MQTT + set, label e remove. Lasciare vuoto per disattivare. Remove elimina + l'oggetto gruppo C-Gate: pubblicare ON sul topic remove. Le etichette sono + in hex. Non vengono create entità Home Assistant. ha_bridge_diagnostics_enabled: name: Abilita diagnostica del bridge description: >- diff --git a/homeassistant-addon/translations/ja.yaml b/homeassistant-addon/translations/ja.yaml index 336d909f..7dfb0413 100644 --- a/homeassistant-addon/translations/ja.yaml +++ b/homeassistant-addon/translations/ja.yaml @@ -239,6 +239,12 @@ configuration: description: >- C-Bus Scene Module のシーン再生・記録 MQTT コマンドを受け付けます。既定はオフ。多くは Trigger Control シーンを使い、記録はモジュールメモリを上書きします。 + cbus_enable_control_app_id: + name: Enable Control アプリケーションID + description: >- + C-Bus Enable Control アプリケーションID(通常 203)。MQTT の set / label / remove + 書き込み用。空で無効。remove は C-Gate グループオブジェクトを削除します。remove トピックに ON を発行してください。ラベルは + hex 符号化。Home Assistant エンティティは作成しません。 ha_bridge_diagnostics_enabled: name: Bridge 診断を有効化 description: Bridge ヘルスエンティティ(準備状態、接続、キュー深度)を Home Assistant に公開します。デフォルト:有効。 diff --git a/homeassistant-addon/translations/ko.yaml b/homeassistant-addon/translations/ko.yaml index 2a2a8d43..24ffbde2 100644 --- a/homeassistant-addon/translations/ko.yaml +++ b/homeassistant-addon/translations/ko.yaml @@ -235,6 +235,12 @@ configuration: description: >- C-Bus Scene Module에서 장면을 재생하거나 녹음하는 MQTT 명령을 받습니다. 기본 꺼짐: 대부분 Trigger Control 장면을 쓰며, 녹음은 모듈 메모리를 덮어씁니다. + cbus_enable_control_app_id: + name: Enable Control 애플리케이션 ID + description: >- + C-Bus Enable Control 애플리케이션 ID(보통 203). MQTT set, label, remove 쓰기용. 비우면 + 꺼짐. remove는 C-Gate 그룹 객체를 삭제하므로 remove 토픽에 ON을 게시하세요. 라벨은 hex 인코딩. Home + Assistant 엔티티는 만들지 않습니다. ha_bridge_diagnostics_enabled: name: Bridge 진단 활성화 description: "Bridge 상태 엔티티 (준비 상태, 연결, 큐 깊이)를 Home Assistant에 게시합니다. 기본값: 활성화." diff --git a/homeassistant-addon/translations/nl.yaml b/homeassistant-addon/translations/nl.yaml index 6de12862..dabc348a 100644 --- a/homeassistant-addon/translations/nl.yaml +++ b/homeassistant-addon/translations/nl.yaml @@ -316,6 +316,13 @@ configuration: Accepteert MQTT-commando's om scènes op een C-Bus Scene Module af te spelen of op te nemen. Standaard uit: de meeste installs gebruiken Trigger Control, en opnemen overschrijft het modulegeheugen. + cbus_enable_control_app_id: + name: Enable Control-toepassings-ID + description: >- + C-Bus Enable Control-toepassings-ID (meestal 203) voor MQTT-writes set, + label en remove. Leeg laten om uit te schakelen. Remove verwijdert het + C-Gate-groepsobject — publiceer ON op het remove-topic. Labels zijn + hex-gecodeerd. Er worden geen Home Assistant-entiteiten aangemaakt. ha_bridge_diagnostics_enabled: name: Bridge Diagnostiek inschakelen description: >- diff --git a/homeassistant-addon/translations/no.yaml b/homeassistant-addon/translations/no.yaml index 3f045168..61256f24 100644 --- a/homeassistant-addon/translations/no.yaml +++ b/homeassistant-addon/translations/no.yaml @@ -303,6 +303,13 @@ configuration: Godtar MQTT-kommandoer som spiller av eller tar opp scener på en C-Bus Scene Module. Av som standard: de fleste bruker Trigger Control, og opptak overskriver modulminnet. + cbus_enable_control_app_id: + name: Enable Control-applikasjons-ID + description: >- + C-Bus Enable Control-applikasjons-ID (vanligvis 203) for MQTT-skriving + set, label og remove. Tomt slår av. Remove sletter C-Gate-gruppeobjektet — + publiser ON på remove-emnet. Etiketter hex-kodes. Ingen Home + Assistant-entiteter opprettes. ha_bridge_diagnostics_enabled: name: Aktiver bridge-diagnostikk description: >- diff --git a/homeassistant-addon/translations/pl.yaml b/homeassistant-addon/translations/pl.yaml index a6e21eb7..2abba919 100644 --- a/homeassistant-addon/translations/pl.yaml +++ b/homeassistant-addon/translations/pl.yaml @@ -310,6 +310,12 @@ configuration: Przyjmuje polecenia MQTT odtwarzania i nagrywania scen na C-Bus Scene Module. Domyślnie wyłączone: większość używa scen Trigger Control, a zapis nadpisuje pamięć modułu. + cbus_enable_control_app_id: + name: ID aplikacji Enable Control + description: >- + ID aplikacji Enable Control C-Bus (zazwyczaj 203) dla zapisów MQTT set, + label i remove. Puste wyłącza. Remove usuwa obiekt grupy C-Gate — publikuj + ON na temacie remove. Etykiety w hex. Nie tworzy encji Home Assistant. ha_bridge_diagnostics_enabled: name: Włącz diagnostykę bridge description: >- diff --git a/homeassistant-addon/translations/pt.yaml b/homeassistant-addon/translations/pt.yaml index 8a3b6d45..296a0428 100644 --- a/homeassistant-addon/translations/pt.yaml +++ b/homeassistant-addon/translations/pt.yaml @@ -319,6 +319,13 @@ configuration: Aceita comandos MQTT para reproduzir ou gravar cenas num Scene Module C-Bus. Desligado por padrão: a maioria usa cenas de Trigger Control, e gravar sobrescreve a memória do módulo. + cbus_enable_control_app_id: + name: ID da aplicação Enable Control + description: >- + ID da aplicação Enable Control C-Bus (tipicamente 203) para escritas MQTT + set, label e remove. Deixe vazio para desativar. Remove apaga o objeto de + grupo do C-Gate — publique ON no tópico remove. Etiquetas em hex. Não cria + entidades do Home Assistant. ha_bridge_diagnostics_enabled: name: Ativar Diagnósticos do Bridge description: >- diff --git a/homeassistant-addon/translations/ru.yaml b/homeassistant-addon/translations/ru.yaml index 7ea283ad..d01c2031 100644 --- a/homeassistant-addon/translations/ru.yaml +++ b/homeassistant-addon/translations/ru.yaml @@ -319,6 +319,13 @@ configuration: Принимает MQTT-команды воспроизведения и записи сцен на C-Bus Scene Module. По умолчанию выкл.: чаще используют сцены Trigger Control, а запись перезаписывает память модуля. + cbus_enable_control_app_id: + name: ID приложения Enable Control + description: >- + ID приложения Enable Control C-Bus (обычно 203) для MQTT-записей set, + label и remove. Пусто — выкл. Remove удаляет объект группы C-Gate — + публикуйте ON в топик remove. Метки в hex. Сущности Home Assistant не + создаются. ha_bridge_diagnostics_enabled: name: Включить диагностику bridge description: >- diff --git a/homeassistant-addon/translations/sv.yaml b/homeassistant-addon/translations/sv.yaml index 47e71f61..2fb020ce 100644 --- a/homeassistant-addon/translations/sv.yaml +++ b/homeassistant-addon/translations/sv.yaml @@ -307,6 +307,13 @@ configuration: Tar emot MQTT-kommandon som spelar upp eller spelar in scener på en C-Bus Scene Module. Av som standard: de flesta använder Trigger Control, och inspelning skriver över modulens minne. + cbus_enable_control_app_id: + name: Enable Control-applikations-ID + description: >- + C-Bus Enable Control-applikations-ID (vanligen 203) för MQTT-skrivningar + set, label och remove. Tomt inaktiverar. Remove tar bort + C-Gate-gruppobjektet — publicera ON på remove-ämnet. Etiketter hex-kodas. + Inga Home Assistant-entiteter skapas. ha_bridge_diagnostics_enabled: name: Aktivera bridge-diagnostik description: >- diff --git a/homeassistant-addon/translations/uk.yaml b/homeassistant-addon/translations/uk.yaml index 68fc045d..1be18ddd 100644 --- a/homeassistant-addon/translations/uk.yaml +++ b/homeassistant-addon/translations/uk.yaml @@ -321,6 +321,13 @@ configuration: Приймає MQTT-команди відтворення й запису сцен на C-Bus Scene Module. Типово вимкнено: більшість використовує сцени Trigger Control, а запис перезаписує пам'ять модуля. + cbus_enable_control_app_id: + name: ID застосунку Enable Control + description: >- + ID застосунку Enable Control C-Bus (зазвичай 203) для MQTT-записів set, + label і remove. Порожнє вимикає. Remove видаляє об'єкт групи C-Gate — + публікуйте ON у топік remove. Мітки в hex. Сутності Home Assistant не + створюються. ha_bridge_diagnostics_enabled: name: Увімкнути діагностику bridge description: >- diff --git a/homeassistant-addon/translations/zh.yaml b/homeassistant-addon/translations/zh.yaml index c1530f7e..0bb658c6 100644 --- a/homeassistant-addon/translations/zh.yaml +++ b/homeassistant-addon/translations/zh.yaml @@ -200,6 +200,12 @@ configuration: description: >- 接受在 C-Bus Scene Module 上播放或录制场景的 MQTT 命令。默认关闭:多数安装使用 Trigger Control 场景,录制会覆盖模块存储器。 + cbus_enable_control_app_id: + name: Enable Control 应用 ID + description: >- + C-Bus Enable Control 应用 ID(通常为 203),用于 MQTT 的 set、label 和 remove + 写入。留空即关闭。remove 会删除 C-Gate 组对象——请在 remove 主题发布 ON。标签以十六进制编码。不会创建 Home + Assistant 实体。 ha_bridge_diagnostics_enabled: name: 启用 Bridge 诊断 description: 向 Home Assistant 发布 Bridge 健康实体(就绪状态、连接、队列深度)。 默认:启用。 diff --git a/package-lock.json b/package-lock.json index a71e1dc5..12a72a74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cgateweb", - "version": "1.31.0", + "version": "1.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cgateweb", - "version": "1.31.0", + "version": "1.32.0", "license": "MIT", "dependencies": { "adm-zip": "^0.6.0", diff --git a/package.json b/package.json index 989e4051..5afcffb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cgateweb", - "version": "1.31.0", + "version": "1.32.0", "description": "Node.js bridge connecting Clipsal C-Bus automation systems to MQTT for Home Assistant integration", "keywords": [ "cbus", diff --git a/settings.js.example b/settings.js.example index bdf8a0a0..7052e87d 100644 --- a/settings.js.example +++ b/settings.js.example @@ -191,6 +191,11 @@ exports.web_bind_host = '127.0.0.1'; // 'sensor' entities. Also allows injecting readings onto the bus. // exports.cbus_measurement_app_id = '228'; +// --- Enable Control extra verbs (typically application 203) --- +// MQTT-only SET/LABEL/REMOVE (ON/OFF/RAMP already work without this). +// REMOVE deletes the C-Gate group object; the MQTT payload must be ON. +// exports.cbus_enable_control_app_id = '203'; + // ============================================================================= // Reference // ============================================================================= diff --git a/src/cbusCommand.js b/src/cbusCommand.js index eff7a361..a55b25ad 100644 --- a/src/cbusCommand.js +++ b/src/cbusCommand.js @@ -16,6 +16,9 @@ const { MQTT_CMD_TYPE_TEMPERATURE, MQTT_CMD_TYPE_PLAY, MQTT_CMD_TYPE_RECORD, + MQTT_CMD_TYPE_SET, + MQTT_CMD_TYPE_LABEL, + MQTT_CMD_TYPE_REMOVE, MQTT_STATE_ON, MQTT_STATE_OFF, MQTT_COMMAND_STOP, @@ -45,7 +48,10 @@ const VALID_COMMAND_TYPES = new Set([ MQTT_CMD_TYPE_HVAC_FAN_MODE, // HVAC fan mode MQTT_CMD_TYPE_TEMPERATURE, // Temperature Broadcast inject MQTT_CMD_TYPE_PLAY, // Scene Module play - MQTT_CMD_TYPE_RECORD // Scene Module record + MQTT_CMD_TYPE_RECORD, // Scene Module record + MQTT_CMD_TYPE_SET, // Enable Control SET + MQTT_CMD_TYPE_LABEL, // Enable Control LABEL + MQTT_CMD_TYPE_REMOVE // Enable Control REMOVE ]); /** @@ -189,6 +195,9 @@ class CBusCommand { case MQTT_CMD_TYPE_TEMPERATURE: case MQTT_CMD_TYPE_PLAY: case MQTT_CMD_TYPE_RECORD: + case MQTT_CMD_TYPE_SET: + case MQTT_CMD_TYPE_LABEL: + case MQTT_CMD_TYPE_REMOVE: // Payload is used as-is by the command router break; case MQTT_CMD_TYPE_GETALL: diff --git a/src/config/addonOptionMap.js b/src/config/addonOptionMap.js index 9f951c3e..4e6777f1 100644 --- a/src/config/addonOptionMap.js +++ b/src/config/addonOptionMap.js @@ -55,6 +55,10 @@ const ADDON_OPTION_MAP = [ { src: 'cbus_measurement_app_id', dst: 'cbus_measurement_app_id', kind: 'stringifyTruthy' }, { src: 'cbus_clock_enabled', dst: 'cbus_clock_enabled', kind: 'boolDefined' }, { src: 'cbus_scene_module_enabled', dst: 'cbus_scene_module_enabled', kind: 'boolDefined' }, + // Deliberately NOT gated on 'haDiscovery': ENABLE SET|LABEL|REMOVE are + // plain MQTT writes, so an MQTT-only install that opts in must not lose + // them with discovery off. + { src: 'cbus_enable_control_app_id', dst: 'cbus_enable_control_app_id', kind: 'stringifyTruthy' }, { src: 'ha_hvac_temperature_unit', dst: 'ha_hvac_temperature_unit', kind: 'copyTruthy', when: 'haDiscovery' }, { src: 'ha_discovery_auto_type', dst: 'ha_discovery_auto_type', kind: 'boolDefined', when: 'haDiscovery' }, { src: 'ha_discovery_auto_type_name_heuristics', dst: 'ha_discovery_auto_type_name_heuristics', kind: 'boolDefined', when: 'haDiscovery' }, diff --git a/src/config/schema.js b/src/config/schema.js index 249f113c..8ab76b02 100644 --- a/src/config/schema.js +++ b/src/config/schema.js @@ -813,6 +813,15 @@ const SETTINGS_SCHEMA = { exposure: 'both', description: 'Accept MQTT commands that play or record scenes on a C-Bus Scene Module (C-Gate SCENE PLAY|RECORD). Off by default: most installs use Trigger Control scenes instead, and a record write overwrites module memory.' }, + cbus_enable_control_app_id: { + key: 'cbus_enable_control_app_id', + type: 'string', + default: null, + nullable: true, + unit: 'none', + exposure: 'both', + description: 'C-Bus Enable Control app ID (typically 203/$CB); null disables. Gates MQTT writes for C-Gate ENABLE SET|LABEL|REMOVE (cbus/write/{net}/{app}/{group}/set|label|remove). No Home Assistant entities. SET is a native 0–255 byte (ON/OFF accepted as 255/0). LABEL is UTF-8 hex, clipped to 32 characters. REMOVE deletes the C-Gate group object and requires payload ON.' + }, ha_hvac_temperature_unit: { key: 'ha_hvac_temperature_unit', type: 'enum', diff --git a/src/constants.js b/src/constants.js index c5f82b9b..bb8a2352 100644 --- a/src/constants.js +++ b/src/constants.js @@ -8,6 +8,7 @@ const DEFAULT_CBUS_APP_AIRCON = '172'; // C-Bus Air Conditioning applicati const DEFAULT_CBUS_APP_SECURITY = '208'; // C-Bus Security application ($D0) const DEFAULT_CBUS_APP_MEASUREMENT = '228'; // C-Bus Measurement application ($E4) const DEFAULT_CBUS_APP_CLOCK = '223'; // C-Bus Clock and Timekeeping application ($DF) +const DEFAULT_CBUS_APP_ENABLE = '203'; // C-Bus Enable Control application ($CB) const CGATE_LEVEL_MIN = 0; // C-Bus minimum brightness level (off) const CGATE_LEVEL_MAX = 255; // C-Bus maximum brightness level (full brightness) const RAMP_STEP = 26; // 10% of 255, made explicit instead of calculation @@ -132,6 +133,9 @@ const MQTT_CMD_TYPE_HVAC_FAN_MODE = 'fanmode'; // Set HVAC fan mode (automatic const MQTT_CMD_TYPE_TEMPERATURE = 'temperature'; // Inject Temperature Broadcast (°C) const MQTT_CMD_TYPE_PLAY = 'play'; // Scene Module PLAY const MQTT_CMD_TYPE_RECORD = 'record'; // Scene Module RECORD +const MQTT_CMD_TYPE_SET = 'set'; // Enable Control SET (native 0–255) +const MQTT_CMD_TYPE_LABEL = 'label'; // Enable Control LABEL +const MQTT_CMD_TYPE_REMOVE = 'remove'; // Enable Control REMOVE (deletes the C-Gate group) // === Home Assistant Discovery === const HA_COMPONENT_LIGHT = 'light'; @@ -231,6 +235,7 @@ module.exports = { DEFAULT_CBUS_APP_SECURITY, DEFAULT_CBUS_APP_MEASUREMENT, DEFAULT_CBUS_APP_CLOCK, + DEFAULT_CBUS_APP_ENABLE, CGATE_LEVEL_MIN, CGATE_LEVEL_MAX, RAMP_STEP, @@ -320,6 +325,9 @@ module.exports = { MQTT_CMD_TYPE_TEMPERATURE, MQTT_CMD_TYPE_PLAY, MQTT_CMD_TYPE_RECORD, + MQTT_CMD_TYPE_SET, + MQTT_CMD_TYPE_LABEL, + MQTT_CMD_TYPE_REMOVE, // Home Assistant Discovery HA_COMPONENT_LIGHT, diff --git a/src/enableCommand.js b/src/enableCommand.js new file mode 100644 index 00000000..b29b910c --- /dev/null +++ b/src/enableCommand.js @@ -0,0 +1,123 @@ +// @ts-check +'use strict'; + +/** + * Command builders for C-Gate ENABLE SET|LABEL|REMOVE (Enable Control, $CB). + * + * ON/OFF/RAMP already go through the generic lighting-style verbs. These three + * are the extra Enable Control verbs. LABEL puts the group outside the object + * path (same shape as lighting LABEL). Labels are always UTF-8 hex so a `#` + * in the text cannot start a C-Gate comment. REMOVE deletes the C-Gate group + * object, so the MQTT payload must be ON — a retained empty message must not + * wipe groups. + */ + +const ENABLE_LABEL_LANGUAGE_ENGLISH = 1; +const ENABLE_LABEL_MAX_CHARS = 32; + +/** + * Encode text as space-separated uppercase hex bytes for C-Gate `hex`. + * @param {string} text + * @returns {string} + */ +function encodeUtf8ToCgateHex(text) { + return Buffer.from(text, 'utf8') + .toString('hex') + .toUpperCase() + .replace(/(..)/g, '$1 ') + .trim(); +} + +/** + * @param {unknown} payload + * @returns {{ ok: true, value: number } | { ok: false, error: string }} + */ +function parseEnableSetValue(payload) { + const raw = String(payload ?? '').trim(); + const upper = raw.toUpperCase(); + if (upper === 'ON') { + return { ok: true, value: 255 }; + } + if (upper === 'OFF') { + return { ok: true, value: 0 }; + } + if (!/^\d+$/.test(raw)) { + return { ok: false, error: 'expected integer 0–255 or ON/OFF' }; + } + const n = Number(raw); + if (n < 0 || n > 255) { + return { ok: false, error: 'expected integer 0–255 or ON/OFF' }; + } + return { ok: true, value: n }; +} + +/** + * @param {Object} opts + * @param {string} opts.cbusname + * @param {string|number} opts.network + * @param {string|number} opts.application + * @param {string|number} opts.group + * @param {unknown} opts.payload + * @returns {{ ok: true, command: string } | { ok: false, error: string }} + */ +function buildEnableSetCommand({ cbusname, network, application, group, payload }) { + const parsed = parseEnableSetValue(payload); + if (parsed.ok === false) { + return { ok: false, error: parsed.error }; + } + return { + ok: true, + command: `enable set //${cbusname}/${network}/${application}/${group} ${parsed.value}` + }; +} + +/** + * @param {Object} opts + * @param {string} opts.cbusname + * @param {string|number} opts.network + * @param {string|number} opts.application + * @param {string|number} opts.group + * @param {unknown} opts.payload + * @returns {{ ok: true, command: string } | { ok: false, error: string }} + */ +function buildEnableLabelCommand({ cbusname, network, application, group, payload }) { + const raw = String(payload ?? '').trim(); + if (!raw) { + return { ok: false, error: 'label must be non-empty' }; + } + const clipped = Array.from(raw).slice(0, ENABLE_LABEL_MAX_CHARS).join(''); + const hex = encodeUtf8ToCgateHex(clipped); + return { + ok: true, + command: `enable label //${cbusname}/${network}/${application} ${ENABLE_LABEL_LANGUAGE_ENGLISH} ${group} hex ${hex}` + }; +} + +/** + * @param {Object} opts + * @param {string} opts.cbusname + * @param {string|number} opts.network + * @param {string|number} opts.application + * @param {string|number} opts.group + * @param {unknown} opts.payload + * @returns {{ ok: true, command: string } | { ok: false, error: string }} + */ +function buildEnableRemoveCommand({ cbusname, network, application, group, payload }) { + if (String(payload ?? '').trim().toUpperCase() !== 'ON') { + return { ok: false, error: 'remove requires payload ON' }; + } + return { + ok: true, + command: `enable remove //${cbusname}/${network}/${application}/${group}` + }; +} + +module.exports = { + buildEnableSetCommand, + buildEnableLabelCommand, + buildEnableRemoveCommand, + encodeUtf8ToCgateHex, + parseEnableSetValue, + ENABLE_LABEL_LANGUAGE_ENGLISH, + ENABLE_LABEL_MAX_CHARS +}; diff --git a/src/mqttCommandRouter.js b/src/mqttCommandRouter.js index 742aaf84..0a9a3c2a 100644 --- a/src/mqttCommandRouter.js +++ b/src/mqttCommandRouter.js @@ -26,6 +26,9 @@ const { MQTT_CMD_TYPE_TEMPERATURE, MQTT_CMD_TYPE_PLAY, MQTT_CMD_TYPE_RECORD, + MQTT_CMD_TYPE_SET, + MQTT_CMD_TYPE_LABEL, + MQTT_CMD_TYPE_REMOVE, MQTT_TOPIC_SUFFIX_HVAC_SETPOINT, MQTT_TOPIC_SUFFIX_HVAC_MODE, MQTT_TOPIC_SUFFIX_HVAC_FAN_MODE, @@ -62,6 +65,7 @@ const { buildSecurityArmCommand, buildSecurityEmulateKeypadCommand } = require(' const { buildMeasurementDataCommand } = require('./measurementCommand'); const { buildTemperatureBroadcastCommand, celsiusToTemperatureBroadcastByte } = require('./temperatureCommand'); const { buildScenePlayCommand, buildSceneRecordCommand } = require('./sceneCommand'); +const { buildEnableSetCommand, buildEnableLabelCommand, buildEnableRemoveCommand } = require('./enableCommand'); const RateLimiter = require('./web/rateLimiter'); const { UNIT_TABLE: MEASUREMENT_UNIT_TABLE } = require('./applicationDecoders/measurementDecoder'); @@ -337,6 +341,11 @@ class MqttCommandRouter extends EventEmitter { case MQTT_CMD_TYPE_RECORD: this._handleSceneModule(command, payload, topic); break; + case MQTT_CMD_TYPE_SET: + case MQTT_CMD_TYPE_LABEL: + case MQTT_CMD_TYPE_REMOVE: + this._handleEnableControl(command, payload, topic); + break; default: this.logger.warn(`Unrecognized command type: ${commandType}`); } @@ -673,6 +682,47 @@ class MqttCommandRouter extends EventEmitter { this.logger.info(`Scene Module ${command.getCommandType()}: set ${set} scene ${scene}`); } + /** + * Extra Enable Control verbs (C-Gate ENABLE SET|LABEL|REMOVE). + * Gated on cbus_enable_control_app_id (typically 203). ON/OFF/RAMP on the + * same application still use the generic handlers. REMOVE deletes the + * C-Gate group object and requires payload ON. + * @private + */ + _handleEnableControl(command, payload, topic) { + const appId = this.settings.cbus_enable_control_app_id; + if (appId === undefined || appId === null || String(appId).trim() === '' || String(appId) === '0') { + this.logger.warn(`Enable Control command ignored (cbus_enable_control_app_id is unset): ${topic}`); + return; + } + if (String(command.getApplication()) !== String(appId)) { + this.logger.warn(`Enable Control command for non-enable application ${command.getApplication()} on topic ${topic}`); + return; + } + const args = { + cbusname: this.cbusname, + network: command.getNetwork(), + application: command.getApplication(), + group: command.getGroup(), + payload + }; + const commandType = command.getCommandType(); + let result; + if (commandType === MQTT_CMD_TYPE_SET) { + result = buildEnableSetCommand(args); + } else if (commandType === MQTT_CMD_TYPE_LABEL) { + result = buildEnableLabelCommand(args); + } else { + result = buildEnableRemoveCommand(args); + } + if (result.ok === false) { + this.logger.warn(`Enable Control command ignored: ${result.error} on topic ${topic}`); + return; + } + this._queueCommand(result.command + NEWLINE); + this.logger.info(`Enable Control ${commandType}: ${command.getNetwork()}/${command.getApplication()}/${command.getGroup()}`); + } + /** * Handles device tree requests for HA discovery. * @param {CBusCommand} command - The tree request command diff --git a/tests/cbusCommand.test.js b/tests/cbusCommand.test.js index 5a28e752..d86ae61f 100644 --- a/tests/cbusCommand.test.js +++ b/tests/cbusCommand.test.js @@ -572,10 +572,13 @@ describe('CBusCommand', () => { expect(command.isValid()).toBe(false); }); - it('should accept temperature, play and record command types', () => { + it('should accept temperature, play, record, set, label and remove command types', () => { expect(new CBusCommand('cbus/write/254/25/3/temperature', '21.5').isValid()).toBe(true); expect(new CBusCommand('cbus/write/254/203/1/play', '4').isValid()).toBe(true); expect(new CBusCommand('cbus/write/254/203/1/record', '4').isValid()).toBe(true); + expect(new CBusCommand('cbus/write/254/203/7/set', '128').isValid()).toBe(true); + expect(new CBusCommand('cbus/write/254/203/7/label', 'Kitchen').isValid()).toBe(true); + expect(new CBusCommand('cbus/write/254/203/7/remove', 'ON').isValid()).toBe(true); }); }); diff --git a/tests/cbusFeatureCommands.test.js b/tests/cbusFeatureCommands.test.js index 1be68f8c..6bcbe738 100644 --- a/tests/cbusFeatureCommands.test.js +++ b/tests/cbusFeatureCommands.test.js @@ -3,6 +3,12 @@ const { buildClockRequestRefresh } = require('../src/clockCommand'); const { buildTemperatureBroadcastCommand, celsiusToTemperatureBroadcastByte } = require('../src/temperatureCommand'); const { buildScenePlayCommand, buildSceneRecordCommand } = require('../src/sceneCommand'); +const { + buildEnableSetCommand, + buildEnableLabelCommand, + buildEnableRemoveCommand, + encodeUtf8ToCgateHex +} = require('../src/enableCommand'); describe('clockCommand', () => { it('builds CLOCK REQUEST_REFRESH for the network clock app', () => { @@ -32,3 +38,46 @@ describe('sceneCommand', () => { expect(buildSceneRecordCommand({ set: 1, scene: 4 })).toBe('scene record 1 4'); }); }); + +describe('enableCommand', () => { + const base = { cbusname: 'HOME', network: 254, application: 203, group: 7 }; + + it('builds ENABLE SET with a native 0–255 byte', () => { + expect(buildEnableSetCommand({ ...base, payload: '128' })) + .toEqual({ ok: true, command: 'enable set //HOME/254/203/7 128' }); + expect(buildEnableSetCommand({ ...base, payload: 'ON' })) + .toEqual({ ok: true, command: 'enable set //HOME/254/203/7 255' }); + expect(buildEnableSetCommand({ ...base, payload: 'off' })) + .toEqual({ ok: true, command: 'enable set //HOME/254/203/7 0' }); + }); + + it('rejects SET values outside 0–255', () => { + expect(buildEnableSetCommand({ ...base, payload: '256' }).ok).toBe(false); + expect(buildEnableSetCommand({ ...base, payload: '-1' }).ok).toBe(false); + expect(buildEnableSetCommand({ ...base, payload: '1.5' }).ok).toBe(false); + expect(buildEnableSetCommand({ ...base, payload: '' }).ok).toBe(false); + }); + + it('hex-encodes LABEL text and keeps group out of the object path', () => { + expect(encodeUtf8ToCgateHex('Hello')).toBe('48 65 6C 6C 6F'); + expect(buildEnableLabelCommand({ ...base, payload: 'Hello' })) + .toEqual({ ok: true, command: 'enable label //HOME/254/203 1 7 hex 48 65 6C 6C 6F' }); + expect(buildEnableLabelCommand({ ...base, payload: 'a#b' }).command) + .toContain('hex 61 23 62'); + }); + + it('clips LABEL to 32 characters and rejects empty labels', () => { + const long = 'a'.repeat(33); + const result = buildEnableLabelCommand({ ...base, payload: long }); + expect(result.ok).toBe(true); + expect(result.command).toMatch(/hex (?:61 ){31}61$/); + expect(buildEnableLabelCommand({ ...base, payload: ' ' }).ok).toBe(false); + }); + + it('builds ENABLE REMOVE only when the payload is ON', () => { + expect(buildEnableRemoveCommand({ ...base, payload: 'ON' })) + .toEqual({ ok: true, command: 'enable remove //HOME/254/203/7' }); + expect(buildEnableRemoveCommand({ ...base, payload: '' }).ok).toBe(false); + expect(buildEnableRemoveCommand({ ...base, payload: 'OFF' }).ok).toBe(false); + }); +}); diff --git a/tests/config/configLoader.test.js b/tests/config/configLoader.test.js index d5e9324b..2682982d 100644 --- a/tests/config/configLoader.test.js +++ b/tests/config/configLoader.test.js @@ -571,6 +571,16 @@ describe('ConfigLoader', () => { expect(config.cbus_measurement_app_id).toBe('228'); }); + + test('should map cbus_enable_control_app_id to config as a string even when HA discovery is disabled', () => { + const options = { ...mockAddonOptions, ha_discovery_enabled: false, cbus_enable_control_app_id: 203 }; + fs.existsSync.mockReturnValue(true); + fs.readFileSync.mockReturnValue(JSON.stringify(options)); + + const config = configLoader.load(); + + expect(config.cbus_enable_control_app_id).toBe('203'); + }); }); describe('load() - Standalone Configuration', () => { diff --git a/tests/defaultSettings.test.js b/tests/defaultSettings.test.js index 40a5aa4d..bed0add8 100644 --- a/tests/defaultSettings.test.js +++ b/tests/defaultSettings.test.js @@ -11,14 +11,14 @@ describe('defaultSettings — frozen baseline', () => { expect(defaultSettings).toStrictEqual(defaultSettingsSnapshot); }); - it('exports the same 138 keys as the baseline', () => { + it('exports the same 139 keys as the baseline', () => { expect(Object.keys(defaultSettings).sort()) .toEqual(Object.keys(defaultSettingsSnapshot).sort()); // Bumping this count is only ever legitimate alongside a genuinely NEW // setting added to the fixture; a changed value for an existing key is // the thing this baseline exists to catch, and must never be "fixed" // in the fixture. - expect(Object.keys(defaultSettings)).toHaveLength(138); + expect(Object.keys(defaultSettings)).toHaveLength(139); }); it('returns a fresh object so consumers cannot mutate the schema defaults', () => { @@ -67,6 +67,12 @@ describe('scene module default', () => { }); }); +describe('enable control app id default', () => { + it('defaults cbus_enable_control_app_id to null (disabled)', () => { + expect(defaultSettings.cbus_enable_control_app_id).toBeNull(); + }); +}); + describe('unlisted group discovery default', () => { it('defaults ha_discovery_unlisted_groups to false (retained configs are hard to clean up)', () => { expect(defaultSettings.ha_discovery_unlisted_groups).toBe(false); diff --git a/tests/fixtures/defaultSettings.snapshot.json b/tests/fixtures/defaultSettings.snapshot.json index f9e29f0a..8f1114d3 100644 --- a/tests/fixtures/defaultSettings.snapshot.json +++ b/tests/fixtures/defaultSettings.snapshot.json @@ -5,6 +5,7 @@ "cbus_aircon_app_id": null, "cbus_aircon_control_enabled": false, "cbus_clock_enabled": false, + "cbus_enable_control_app_id": null, "cbus_label_file": null, "cbus_measurement_app_id": null, "cbus_scene_module_enabled": false, diff --git a/tests/mqttCommandRouter.test.js b/tests/mqttCommandRouter.test.js index e0746684..bee08e73 100644 --- a/tests/mqttCommandRouter.test.js +++ b/tests/mqttCommandRouter.test.js @@ -1401,4 +1401,41 @@ describe('MqttCommandRouter', () => { warnSpy.mockRestore(); }); }); + + describe('enable control set/label/remove', () => { + it('ignores set when the app id is unset', () => { + const warnSpy = jest.spyOn(router.logger, 'warn'); + router.routeMessage('cbus/write/254/203/7/set', '128'); + expect(mockQueue.add).not.toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('cbus_enable_control_app_id is unset')); + warnSpy.mockRestore(); + }); + + it('sends ENABLE SET, LABEL and REMOVE when the app id matches', () => { + router.settings.cbus_enable_control_app_id = '203'; + router.routeMessage('cbus/write/254/203/7/set', '128'); + expect(mockQueue.add).toHaveBeenCalledWith('enable set //TestProject/254/203/7 128\n'); + mockQueue.add.mockClear(); + router.routeMessage('cbus/write/254/203/7/label', 'Hello'); + expect(mockQueue.add).toHaveBeenCalledWith('enable label //TestProject/254/203 1 7 hex 48 65 6C 6C 6F\n'); + mockQueue.add.mockClear(); + router.routeMessage('cbus/write/254/203/7/remove', 'ON'); + expect(mockQueue.add).toHaveBeenCalledWith('enable remove //TestProject/254/203/7\n'); + }); + + it('rejects remove without payload ON so a retained empty message cannot delete groups', () => { + router.settings.cbus_enable_control_app_id = '203'; + const warnSpy = jest.spyOn(router.logger, 'warn'); + router.routeMessage('cbus/write/254/203/7/remove', ''); + expect(mockQueue.add).not.toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('remove requires payload ON')); + warnSpy.mockRestore(); + }); + + it('ignores enable verbs on a different application', () => { + router.settings.cbus_enable_control_app_id = '203'; + router.routeMessage('cbus/write/254/56/7/set', '128'); + expect(mockQueue.add).not.toHaveBeenCalled(); + }); + }); });