node-red-contrib-ccu 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ /* Semantic roles of channels and the Home Assistant facts of datapoints.
2
+ Ported from hm2mqtt.js lib/roles.js (same author) for B-16.
3
+
4
+ hm2mqtt derives the role from the CONTROL hint eQ-3 puts on every VALUES
5
+ parameter. In the paramset cache shipped with this package (and therefore
6
+ in the persisted cache of existing installs) only about half of the VALUES
7
+ descriptions carry CONTROL fields — the older contributed user dumps were
8
+ stripped of them — so here the channel TYPE is the primary key and CONTROL
9
+ only refines where the description has it. Pure functions and tables, no I/O. */
10
+
11
+ /** CONTROL hint of a channel's primary datapoint → role; checked in this order */
12
+ const ROLE_BY_CONTROL = [
13
+ ['LOCK.STATE', 'lock'],
14
+ ['HEATING_CONTROL_HMIP.SETPOINT', 'climate_hmip'],
15
+ ['HEATING_CONTROL.SETPOINT', 'climate_hm'],
16
+ ['BLIND.LEVEL', 'cover'],
17
+ ['BLIND_VIRTUAL_RECEIVER.LEVEL', 'cover'],
18
+ ['SHUTTER_VIRTUAL_RECEIVER.LEVEL', 'cover'],
19
+ ['BLIND_TRANSMITTER.LEVEL', 'cover_state'],
20
+ ['SHUTTER_TRANSMITTER.LEVEL', 'cover_state'],
21
+ ['DIMMER.LEVEL', 'dimmer'],
22
+ ['DIMMER_REAL.LEVEL', 'dimmer_state'],
23
+ ['SWITCH.STATE', 'switch'],
24
+ ['SWITCH_TRANSMITTER.STATE', 'switch_state'],
25
+ ['DANGER.STATE', 'smoke'],
26
+ ['DOOR_SENSOR.STATE', 'contact'],
27
+ ['RHS.STATE', 'rotary_handle'],
28
+ ['MOTIONDETECTOR_TRANSCEIVER.MOTION_DETECTION_STATE', 'motion'],
29
+ ['POWERMETER.POWER', 'energy'],
30
+ ['POWERMETER_PSM.POWER', 'energy'],
31
+ ['WEATHER_TRANSMIT.ACTUAL_TEMPERATURE', 'weather'],
32
+ ['BUTTON.SHORT', 'key'],
33
+ ['BUTTON_NO_FUNCTION.SHORT', 'key'],
34
+ ];
35
+
36
+ /** channel type → role (HM classic, HmIP and the CCU's own channels) */
37
+ const ROLE_BY_TYPE = {
38
+ MAINTENANCE: 'maintenance',
39
+
40
+ // HM classic actuators
41
+ SWITCH: 'switch',
42
+ DIMMER: 'dimmer',
43
+ VIRTUAL_DIMMER: 'dimmer',
44
+ BLIND: 'cover',
45
+ KEYMATIC: 'lock',
46
+
47
+ // HmIP actuators: one *_TRANSMITTER (state) followed by *_VIRTUAL_RECEIVER channels (control)
48
+ SWITCH_VIRTUAL_RECEIVER: 'switch',
49
+ SWITCH_TRANSMITTER: 'switch_state',
50
+ DIMMER_VIRTUAL_RECEIVER: 'dimmer',
51
+ DIMMER_TRANSMITTER: 'dimmer_state',
52
+ BLIND_VIRTUAL_RECEIVER: 'cover',
53
+ SHUTTER_VIRTUAL_RECEIVER: 'cover',
54
+ BLIND_TRANSMITTER: 'cover_state',
55
+ SHUTTER_TRANSMITTER: 'cover_state',
56
+
57
+ // thermostats
58
+ HEATING_CLIMATECONTROL_TRANSCEIVER: 'climate_hmip',
59
+ CLIMATECONTROL_RT_TRANSCEIVER: 'climate_hm',
60
+ THERMALCONTROL_TRANSMIT: 'climate_hm',
61
+
62
+ // sensors
63
+ SHUTTER_CONTACT: 'contact',
64
+ SHUTTER_CONTACT_TRANSCEIVER: 'contact',
65
+ MULTI_MODE_INPUT_TRANSMITTER: 'contact',
66
+ TILT_SENSOR: 'contact',
67
+ ROTARY_HANDLE_SENSOR: 'rotary_handle',
68
+ ROTARY_HANDLE_TRANSCEIVER: 'rotary_handle',
69
+ MOTION_DETECTOR: 'motion',
70
+ MOTIONDETECTOR_TRANSCEIVER: 'motion',
71
+ PRESENCEDETECTOR_TRANSCEIVER: 'presence',
72
+ WEATHER: 'weather',
73
+ WEATHER_TRANSMIT: 'weather',
74
+ WATERDETECTIONSENSOR: 'water',
75
+ WATER_DETECTION_TRANSMITTER: 'water',
76
+ SMOKE_DETECTOR: 'smoke',
77
+ SMOKE_DETECTOR_TEAM: 'smoke',
78
+ SMOKE_DETECTOR_TEAM_V2: 'smoke',
79
+ POWERMETER: 'energy',
80
+ ENERGIE_METER_TRANSMITTER: 'energy',
81
+
82
+ // keys
83
+ KEY: 'key',
84
+ VIRTUAL_KEY: 'key',
85
+ KEY_TRANSCEIVER: 'key',
86
+ CENTRAL_KEY: 'key',
87
+ };
88
+
89
+ /**
90
+ * @param {string} channelType
91
+ * @param {object} [description] VALUES paramset description of the channel
92
+ * @returns {string | null}
93
+ */
94
+ function channelRole(channelType, description) {
95
+ if (ROLE_BY_TYPE[channelType] === 'maintenance') {
96
+ return 'maintenance';
97
+ }
98
+
99
+ if (description) {
100
+ const controls = new Set(Object.values(description).map((d) => d && d.CONTROL));
101
+ for (const [control, role] of ROLE_BY_CONTROL) {
102
+ if (controls.has(control)) {
103
+ return role;
104
+ }
105
+ }
106
+ }
107
+
108
+ return ROLE_BY_TYPE[channelType] || null;
109
+ }
110
+
111
+ /** units as the interface processes send them → Home Assistant */
112
+ function haUnit(unit) {
113
+ if (!unit || unit === '""') {
114
+ return undefined;
115
+ }
116
+
117
+ const u = String(unit).replace(/�|°/g, '°');
118
+ return (
119
+ {
120
+ '100%': '%',
121
+ '% rF': '%',
122
+ Lux: 'lx',
123
+ degree: '°',
124
+ '°': '°',
125
+ min: 'min',
126
+ s: 's',
127
+ mA: 'mA',
128
+ }[u] || u
129
+ );
130
+ }
131
+
132
+ /** `100%` values are 0..1 on the wire */
133
+ function isFraction(description) {
134
+ return Boolean(description && description.UNIT === '100%');
135
+ }
136
+
137
+ /**
138
+ * Home Assistant facts of a datapoint by name: device class, state class, unit override,
139
+ * entity category, enabled by default. Anything not listed is a plain sensor / binary_sensor,
140
+ * disabled by default.
141
+ */
142
+ const PARAMETERS = {
143
+ ACTUAL_TEMPERATURE: {dev_cla: 'temperature', stat_cla: 'measurement', unit: '°C', enabled: true},
144
+ TEMPERATURE: {dev_cla: 'temperature', stat_cla: 'measurement', unit: '°C', enabled: true},
145
+ SET_POINT_TEMPERATURE: {dev_cla: 'temperature', unit: '°C'},
146
+ SET_TEMPERATURE: {dev_cla: 'temperature', unit: '°C'},
147
+ HUMIDITY: {dev_cla: 'humidity', stat_cla: 'measurement', unit: '%', enabled: true},
148
+ ACTUAL_HUMIDITY: {dev_cla: 'humidity', stat_cla: 'measurement', unit: '%', enabled: true},
149
+ ILLUMINATION: {dev_cla: 'illuminance', stat_cla: 'measurement', unit: 'lx', enabled: true},
150
+ CURRENT_ILLUMINATION: {dev_cla: 'illuminance', stat_cla: 'measurement', unit: 'lx'},
151
+ AVERAGE_ILLUMINATION: {dev_cla: 'illuminance', stat_cla: 'measurement', unit: 'lx'},
152
+ BRIGHTNESS: {stat_cla: 'measurement', enabled: true},
153
+ POWER: {dev_cla: 'power', stat_cla: 'measurement', unit: 'W', enabled: true},
154
+ CURRENT: {dev_cla: 'current', stat_cla: 'measurement', unit: 'mA', enabled: true},
155
+ VOLTAGE: {dev_cla: 'voltage', stat_cla: 'measurement', unit: 'V', enabled: true},
156
+ FREQUENCY: {dev_cla: 'frequency', stat_cla: 'measurement', unit: 'Hz'},
157
+ ENERGY_COUNTER: {dev_cla: 'energy', stat_cla: 'total_increasing', unit: 'Wh', enabled: true},
158
+ RAIN_COUNTER: {dev_cla: 'precipitation', stat_cla: 'total_increasing', unit: 'mm', enabled: true},
159
+ RAINING: {dev_cla: 'moisture', enabled: true},
160
+ WIND_SPEED: {dev_cla: 'wind_speed', stat_cla: 'measurement', unit: 'km/h', enabled: true},
161
+ WIND_DIRECTION: {stat_cla: 'measurement', unit: '°', enabled: true},
162
+ WIND_DIR: {stat_cla: 'measurement', unit: '°', enabled: true},
163
+ SUNSHINEDURATION: {stat_cla: 'total_increasing', enabled: true},
164
+ AIR_PRESSURE: {dev_cla: 'atmospheric_pressure', stat_cla: 'measurement', unit: 'hPa', enabled: true},
165
+ VALVE_STATE: {stat_cla: 'measurement', unit: '%', enabled: true},
166
+ LEVEL: {stat_cla: 'measurement', unit: '%'},
167
+ BOOST_STATE: {unit: 'min'},
168
+ LOW_BAT: {dev_cla: 'battery', ent_cat: 'diagnostic', enabled: true},
169
+ LOWBAT: {dev_cla: 'battery', ent_cat: 'diagnostic', enabled: true},
170
+ UNREACH: {dev_cla: 'connectivity', ent_cat: 'diagnostic', inverted: true},
171
+ STICKY_UNREACH: {dev_cla: 'problem', ent_cat: 'diagnostic'},
172
+ CONFIG_PENDING: {ent_cat: 'diagnostic'},
173
+ UPDATE_PENDING: {dev_cla: 'update', ent_cat: 'diagnostic'},
174
+ DUTY_CYCLE: {dev_cla: 'problem', ent_cat: 'diagnostic'},
175
+ DUTYCYCLE: {dev_cla: 'problem', ent_cat: 'diagnostic'},
176
+ SABOTAGE: {dev_cla: 'tamper', ent_cat: 'diagnostic'},
177
+ ERROR_SABOTAGE: {dev_cla: 'tamper', ent_cat: 'diagnostic'},
178
+ RSSI_DEVICE: {dev_cla: 'signal_strength', stat_cla: 'measurement', unit: 'dBm', ent_cat: 'diagnostic'},
179
+ RSSI_PEER: {dev_cla: 'signal_strength', stat_cla: 'measurement', unit: 'dBm', ent_cat: 'diagnostic'},
180
+ OPERATING_VOLTAGE: {dev_cla: 'voltage', stat_cla: 'measurement', unit: 'V', ent_cat: 'diagnostic'},
181
+ ERROR_CODE: {ent_cat: 'diagnostic'},
182
+ WORKING: {dev_cla: 'running'},
183
+ PROCESS: {dev_cla: 'running'},
184
+ MOTION: {dev_cla: 'motion', enabled: true},
185
+ PRESENCE_DETECTION_STATE: {dev_cla: 'occupancy', enabled: true},
186
+ ALARMSTATE: {dev_cla: 'safety', enabled: true},
187
+ MOISTURE_DETECTED: {dev_cla: 'moisture', enabled: true},
188
+ WATERLEVEL_DETECTED: {dev_cla: 'moisture', enabled: true},
189
+ ON_TIME: {ent_cat: 'config', unit: 's'},
190
+ RAMP_TIME: {ent_cat: 'config', unit: 's'},
191
+ };
192
+
193
+ module.exports = {ROLE_BY_CONTROL, ROLE_BY_TYPE, channelRole, haUnit, isFraction, PARAMETERS};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ccu",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "license": "MIT",
5
5
  "main": "none",
6
6
  "keywords": [
@@ -99,6 +99,7 @@
99
99
  "ccu-get-value": "nodes/ccu-get-value.js",
100
100
  "ccu-switch": "nodes/ccu-switch.js",
101
101
  "ccu-hm2mqtt": "nodes/ccu-mqtt.js",
102
+ "ccu-homeassistant": "nodes/ccu-homeassistant.js",
102
103
  "ccu-alexa": "nodes/ccu-alexa.js"
103
104
  }
104
105
  },
@@ -0,0 +1,31 @@
1
+ # B-16 — ccu-homeassistant: Home Assistant MQTT auto-discovery node
2
+
3
+ **✅ done 2026-09-04** (released as 4.2.0) — user request 2026-09-04.
4
+
5
+ New node `ccu-homeassistant`: publishes device-based Home Assistant MQTT
6
+ discovery configs (HA ≥ 2024.11) for a checkbox-selected set of devices.
7
+ Companion to a ccu-mqtt node whose topic templates and payload format it
8
+ reuses, so state and commands keep flowing through ccu-mqtt and the node
9
+ itself only emits the retained discovery configs (plus the aggregate
10
+ `<channel>/PRESS` item for event entities); unticked devices get an empty
11
+ retained config. Mapping ported from hm2mqtt.js (`lib/haroles.js`,
12
+ `lib/hadiscovery.js`, channel TYPE as primary role key because only about
13
+ half of the shipped VALUES descriptions carry CONTROL hints). Editor: device
14
+ table modeled on RedMatic-HomeKit (filter, check/uncheck all), ccu-mqtt
15
+ select, prefix, generic-entities option; `type=devices` admin endpoint.
16
+ Rode along: ccu-mqtt HA command words (OPEN/CLOSE/STOP/ON/OFF, HM
17
+ `*-MODE`), empty-name topic fix, connection `deregister` fix.
18
+
19
+ Verified 2026-09-04 on the two lab CCUs against Home Assistant 2026.9.0
20
+ (docker) and she 1.51.1 (the author's dev instance on the test broker plus
21
+ a private instance): DRS8 switches with round trip, DRI16 inputs + events,
22
+ DRAP sensors, PDT light with brightness (plain payloads), WRC2 events,
23
+ removal on untick, editor smoke test by the user. Three payload defects
24
+ found only through the live test (availability-list `val_tpl`, switch
25
+ `stat_on`/`stat_off`, binary-only device classes on sensors) — the first
26
+ two are inherited from hm2mqtt.js / mqtt-interfaces-core and should be
27
+ fixed upstream. Not covered by lab hardware: cover and climate payloads
28
+ (unit tests only), real key presses. Known gaps (as upstream): RGBW lights,
29
+ sirens, garage doors, sysvars/programs as entities.
30
+
31
+ Original item: | B-16 | **New node `ccu-homeassistant`: Home Assistant MQTT auto-discovery for a checkbox-selected set of devices** (decided 2026-09-04: a dedicated node, not an option on ccu-mqtt). It is a companion to an existing ccu-mqtt node, not a replacement: its config references one ccu-mqtt node of the flow (select populated via `RED.nodes.eachNode`, type `ccu-mqtt`, same ccu-connection) and **reuses that node's configured topics** — `topicOutputEvent` as HA state topics, `topicInputSetValue` as command topics, `topicOutputSysvar` for sysvars later — plus its payload format (`payloadOutput` mqsh-basic/extended/plain → matching `value_template`, e.g. `{{ value_json.val }}`). State and commands keep flowing through ccu-mqtt; the new node only emits the retained discovery configs `{topic, payload, retain: true}` for the same downstream mqtt-out node (no broker connection of its own), republishes them on deploy/reconnect, and publishes empty retained configs for devices that were unchecked (removal from HA). **Device selection like RedMatic-HomeKit's `redmatic-homekit-homematic-devices` node**: the config dialog loads all devices of the connection from the `/ccu` admin endpoint into a table (checkbox · name · address · type, "enable all"/"disable all" buttons, filter box, optionally expandable per-channel rows), stored as a `devices` object keyed by address — but **opt-in**: nothing is checked, hence nothing published, until the user ticks a device. Mapping modeled on hm2mqtt.js [`lib/hadiscovery.js`](https://github.com/hobbyquaker/hm2mqtt.js/blob/master/lib/hadiscovery.js) (README section "Home Assistant"): one HA device per Homematic device, device-based discovery payloads `homeassistant/device/<id>/config` (HA ≥ 2024.11; prefix configurable), entities derived from the paramset descriptions' `CONTROL` hints — switch (SWITCH.STATE, HmIP virtual receivers), light (dimmers), cover (blind position/tilt/direction), climate (HM/HmIP thermostats, modes/presets), binary_sensor (contact/motion/presence/smoke/water), event (key press short/long), lock (KEYMATIC), sensor (energy, weather, maintenance) — port hm2mqtt's mapping table rather than re-deriving it. **Constraint found 2026-09-04 (corrected the same day):** only 290 of the 647 VALUES entries in the shipped `paramsets.json` carry `CONTROL` hints (the older contributed user dumps were stripped of them), and the persisted runtime cache of existing installs inherits that — so a pure CONTROL-based mapping would silently skip roughly half the device types. Key the mapping on the channel `TYPE` from the device metadata (always present) and use CONTROL only as a refinement where the cache has it (live fetches and the B-1 regen bring CONTROL in; the fetch tool stores the full response). B-3 is not a prerequisite (HA commands use the setValue path, which casts in the connection), but its trivial fixes ride along. Builds on Phase 3's extracted `lib/topic.js` so the HA node computes exactly ccu-mqtt's topics. **Runs on branch `b-16-homeassistant`, target 4.2.0** (decided 2026-09-04; 4.1.0 stays the small data release). **Status 2026-09-04: implemented on the branch** — `nodes/lib/haroles.js` + `nodes/lib/hadiscovery.js` (pure, 24 unit tests on paramsets.json fixtures), `nodes/ccu-homeassistant.{js,html}` (device table, ccu-mqtt reference, republish on input, removal configs), `type=devices` admin endpoint, ccu-mqtt command words (OPEN/CLOSE/STOP/ON/OFF, HM `*-MODE`) + empty-name topic fix + connection `deregister` fix, README section, `examples/home-assistant.json`, CHANGELOG "Unreleased (4.2.0)". **Verified end-to-end 2026-09-04** on the lab CCU3 (RedMatic 9.0.0-alpha, aedes broker inside Node-RED) against Home Assistant 2026.9.0 (docker): HmIPW-DRS8 → 8 switches (state from the transmitter channel, secondaries disabled), HmIPW-DRI16 → 16 opening sensors + 16 event entities, HmIPW-DRAP → bus voltage/current sensors; switch round trip HA → `hm/set` → CCU → `hm/status` → HA state on/off; untick + deploy removes the device from HA. Three payload defects found and fixed only because of the live test (HA rejected/ignored silently): an availability-list entry takes `val_tpl`, not `avty_tpl`; a switch needs explicit `stat_on`/`stat_off` because HA compares the templated state with the command payloads by default; a sensor/number must not carry a binary_sensor class (`running` on the ENUM datapoint PROCESS). The first two are inherited from hm2mqtt.js / mqtt-interfaces-core and should be fixed upstream too. Also fixed: the readiness wait treated an empty VALUES paramset as "not cached" (5-minute fallback delay). Second round on the OpenCCU lab box (same HA, second ccu-mqtt with prefix `hm2/` and **plain payloads**, so the non-JSON templates are covered too): HmIP-PDT → light with brightness (HA brightness 128 → `LEVEL` 0.5 → state on/128 from the DIMMER_TRANSMITTER channel, off → 0, plain on → last brightness), HmIP-WRC2 → 2 event entities + battery sensor, no HA warnings. **Second consumer:** [she](https://github.com/hobbyquaker/she) 1.51.1 (the author's dev instance on the test broker, which both lab flows now feed as well) parses the same retained payloads (`GET /she/mqtt/ha-discovery`) into the 5 lab devices with plausible entity groups and no orphaned/duplicate flags — she is the lighter, scriptable check for future B-16 changes; HA stays the reference for schema validation. Not covered by the lab: cover and climate payloads (no blind or thermostat paired anywhere), key presses on real hardware. **Open before merge:** editor smoke test in a browser (device table, ccu-mqtt select), CI run (the local mocha suite cannot bind port 8181 under WSL); cover/climate stay unit-test-only until a device is available. Merge after 4.1.0 ships. Known gaps upstream too: RGBW lights, sirens, garage doors, sysvars/programs as entities. | user request |