node-red-contrib-ccu 4.6.0 → 4.6.1

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ Notable changes to node-red-contrib-ccu. Format follows
5
5
  user-visible symptom and the cause, not the commit list (the release notes
6
6
  append commits automatically).
7
7
 
8
+ ## 4.6.1 (2026-10-22)
9
+
10
+ ### Fixed
11
+
12
+ - **Home Assistant logged an error for every published device: the
13
+ maintenance channel's `STICKY_UNREACH`** went out as a `switch` with
14
+ `device_class: problem`, a class only `binary_sensor` knows, and Home
15
+ Assistant refused that component ("expected SwitchDeviceClass or one of
16
+ 'outlet', 'switch'") while it created the device and its other entities
17
+ (B-39). `STICKY_UNREACH` is no longer announced at all - the maintainer's
18
+ decision: "UNREACH is enough" -, and a device class a platform does not
19
+ accept is dropped from any component before it goes out. A test now checks
20
+ every component of every device the shipped paramsets know against Home
21
+ Assistant's device classes per platform.
22
+
8
23
  ## 4.6.0 (2026-10-22)
9
24
 
10
25
  ### Added
@@ -19,6 +19,122 @@ const TILT_TYPES = /BLIND/;
19
19
  /** Platforms without a state topic. */
20
20
  const STATELESS = new Set(['button', 'notify', 'scene', 'tag']);
21
21
 
22
+ /**
23
+ * The device classes Home Assistant accepts per platform (B-39). A component whose class is not in
24
+ * its platform's list is refused by Home Assistant together with its error log line, so `e()` drops
25
+ * such a class and the unit tests check every component of every known device against this table.
26
+ * Platforms missing here (light, climate, lock, select) take no device class at all.
27
+ */
28
+ const SENSOR_CLASSES = [
29
+ 'absolute_humidity',
30
+ 'apparent_power',
31
+ 'aqi',
32
+ 'area',
33
+ 'atmospheric_pressure',
34
+ 'battery',
35
+ 'blood_glucose_concentration',
36
+ 'carbon_dioxide',
37
+ 'carbon_monoxide',
38
+ 'conductivity',
39
+ 'current',
40
+ 'data_rate',
41
+ 'data_size',
42
+ 'distance',
43
+ 'duration',
44
+ 'energy',
45
+ 'energy_distance',
46
+ 'energy_storage',
47
+ 'frequency',
48
+ 'gas',
49
+ 'humidity',
50
+ 'illuminance',
51
+ 'irradiance',
52
+ 'moisture',
53
+ 'monetary',
54
+ 'nitrogen_dioxide',
55
+ 'nitrogen_monoxide',
56
+ 'nitrous_oxide',
57
+ 'ozone',
58
+ 'ph',
59
+ 'pm1',
60
+ 'pm10',
61
+ 'pm25',
62
+ 'pm4',
63
+ 'power',
64
+ 'power_factor',
65
+ 'precipitation',
66
+ 'precipitation_intensity',
67
+ 'pressure',
68
+ 'radon',
69
+ 'reactive_energy',
70
+ 'reactive_power',
71
+ 'signal_strength',
72
+ 'sound_pressure',
73
+ 'speed',
74
+ 'sulphur_dioxide',
75
+ 'temperature',
76
+ 'temperature_delta',
77
+ 'volatile_organic_compounds',
78
+ 'volatile_organic_compounds_parts',
79
+ 'voltage',
80
+ 'volume',
81
+ 'volume_flow_rate',
82
+ 'volume_storage',
83
+ 'water',
84
+ 'weight',
85
+ 'wind_direction',
86
+ 'wind_speed',
87
+ ];
88
+ const DEVICE_CLASSES = {
89
+ binary_sensor: new Set([
90
+ 'battery',
91
+ 'battery_charging',
92
+ 'carbon_monoxide',
93
+ 'cold',
94
+ 'connectivity',
95
+ 'door',
96
+ 'garage_door',
97
+ 'gas',
98
+ 'heat',
99
+ 'light',
100
+ 'lock',
101
+ 'moisture',
102
+ 'motion',
103
+ 'moving',
104
+ 'occupancy',
105
+ 'opening',
106
+ 'plug',
107
+ 'power',
108
+ 'presence',
109
+ 'problem',
110
+ 'running',
111
+ 'safety',
112
+ 'smoke',
113
+ 'sound',
114
+ 'tamper',
115
+ 'update',
116
+ 'vibration',
117
+ 'window',
118
+ ]),
119
+ sensor: new Set([...SENSOR_CLASSES, 'date', 'enum', 'timestamp', 'uptime']),
120
+ number: new Set(SENSOR_CLASSES),
121
+ switch: new Set(['outlet', 'switch']),
122
+ button: new Set(['identify', 'restart', 'update']),
123
+ cover: new Set(['awning', 'blind', 'curtain', 'damper', 'door', 'garage', 'gate', 'shade', 'shutter', 'window']),
124
+ event: new Set(['button', 'doorbell', 'motion']),
125
+ };
126
+
127
+ /** true when Home Assistant accepts `deviceClass` on `platform` */
128
+ function validDeviceClass(platform, deviceClass) {
129
+ return Boolean(DEVICE_CLASSES[platform] && DEVICE_CLASSES[platform].has(deviceClass));
130
+ }
131
+
132
+ /**
133
+ * Datapoints that are never announced (B-39; the maintainer, 2026-10-22: "don't publish
134
+ * STICKY_UNREACH to home assistant auto discovery. UNREACH is enough.").
135
+ */
136
+ const NOT_ANNOUNCED = new Set(['STICKY_UNREACH']);
137
+
22
138
  /** device classes that only binary_sensor knows — a sensor/number with one of them is rejected by HA */
23
139
  const BINARY_ONLY_CLASSES = new Set([
24
140
  'running',
@@ -267,7 +383,7 @@ function deviceBlock(ctx, iface, device) {
267
383
  const e = (dp, platform, label, more = {}) => {
268
384
  const stateless = STATELESS.has(platform);
269
385
  const uid = more.uid || `${ch.index}_${dp}`;
270
- return compact({
386
+ const component = compact({
271
387
  p: platform,
272
388
  uniq_id: `${id}_${uid}`,
273
389
  name: label,
@@ -276,6 +392,12 @@ function deviceBlock(ctx, iface, device) {
276
392
  ...(more.command && {cmd_t: scmd(dp)}),
277
393
  ...(more.extra || {}),
278
394
  });
395
+ // a class the platform does not know makes HA refuse the component (B-39)
396
+ if (component.dev_cla !== undefined && !validDeviceClass(platform, component.dev_cla)) {
397
+ delete component.dev_cla;
398
+ }
399
+
400
+ return component;
279
401
  };
280
402
 
281
403
  const fallback = ch.index === 0 ? 'Maintenance' : `Channel ${ch.index}`;
@@ -659,7 +781,7 @@ function deviceBlock(ctx, iface, device) {
659
781
 
660
782
  // generic layer: everything else in the channel's VALUES description
661
783
  for (const [dp, d] of Object.entries(ch.description)) {
662
- if (consumed.has(dp) || !d || typeof d !== 'object') {
784
+ if (consumed.has(dp) || NOT_ANNOUNCED.has(dp) || !d || typeof d !== 'object') {
663
785
  continue;
664
786
  }
665
787
 
@@ -879,6 +1001,9 @@ function discoveryMessages(ctx) {
879
1001
  }
880
1002
 
881
1003
  module.exports = {
1004
+ DEVICE_CLASSES,
1005
+ NOT_ANNOUNCED,
1006
+ validDeviceClass,
882
1007
  discoveryId,
883
1008
  discoveryTopic,
884
1009
  ifaceOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ccu",
3
- "version": "4.6.0",
3
+ "version": "4.6.1",
4
4
  "license": "MIT",
5
5
  "main": "none",
6
6
  "keywords": [