optolink-bridge 1.1.2 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  This file documents all *major & minor* releases. For revisions, please consult the [commit history](https://github.com/kristian/optolink-bridge/commits/main).
4
4
 
5
+ ## [1.3] - 2026-04-12
6
+
7
+ Introduce a `max_buffer_chunks` option to prevent stalled streams from filling up the memory with too many buffered chunks, and expose the vito + opto serial ports and bridges on the event emitter for debugging purposes.
8
+
9
+ ## [1.2] - 2026-01-08
10
+
11
+ Introduce a `publish_bus_state` MQTT option
12
+
13
+ ## [1.1] - 2025-12-17
14
+
15
+ Major dependencies bump & allow Vitoconnect to reset the synchronization
16
+
5
17
  ## [1.0] - 2025-02-26
6
18
 
7
19
  Initial release
package/config.toml CHANGED
@@ -132,6 +132,9 @@ max_decimals = 4
132
132
  # (this is because if poll_items is empty, optolink-bridge will start in pass-through mode and is not able to intercept messages)
133
133
  auto_reload_addr_items = true
134
134
 
135
+ # the maximum number of chunks to buffer in one direction, before considering the stream to be stalled and dropping oldest chunks, defaults to 1000
136
+ max_buffered_chunks = 100
137
+
135
138
  [mqtt]
136
139
 
137
140
  # broker protocol / host / port
@@ -150,6 +153,10 @@ suffix = "<dpname>"
150
153
  # note: if set to false, home assistant will not be able to determine if the devices / entities are available
151
154
  online = true
152
155
 
156
+ # publish a "bus_state" topic, e.g. Vito/bus_state, indicating the current state of the optolink bus. defaults to true /
157
+ # "bus_state", set to a string to use a different topic. possible bus states are: 0: syncing, 1: synced, 2: flowing
158
+ publish_bus_state = true
159
+
153
160
  # publish unknown data points to MQTT as raw data
154
161
  publish_unknown_dps = false
155
162
  # the suffix to use to publish unknown data points, defaults to "raw/<addr>"
package/index.js CHANGED
@@ -12,6 +12,17 @@ import { publishDevice } from './discovery.js';
12
12
  let logLevel, trace, logger = {};
13
13
  let dps, pollQueue = new Queue(), pollIntervals;
14
14
  let busState = 0; // 0: syncing, 1: synced, 2: flowing
15
+ let afterBusStateUpdate = async (newState, oldState) => {};
16
+ async function updateBusState(state) {
17
+ const oldState = busState; busState = state;
18
+ if (typeof afterBusStateUpdate === 'function') {
19
+ try {
20
+ await afterBusStateUpdate(state, oldState);
21
+ } catch {
22
+ // ignore errors
23
+ }
24
+ }
25
+ }
15
26
  let deviceSerialNo = undefined, deviceDiscoveryOptions = { enabled: true }, deviceDiscovery;
16
27
 
17
28
  async function applyConfig(config) {
@@ -113,10 +124,23 @@ deviceDiscovery = async function(options = (deviceDiscoveryOptions ?? { enabled:
113
124
  }
114
125
 
115
126
  if (config.mqtt && config.mqtt.url) {
116
- config.mqtt.online ??= true; // if not set, use a online topic
117
- mqttTopic = config.mqtt.topic ?? 'Vito', mqttAvailabilityTopic =
118
- `${mqttTopic}${mqttTopic.endsWith('/') || config.mqtt.online.startsWith?.('/') ? '' : '/'}${
119
- typeof config.mqtt.online === 'string' ? config.mqtt.online : 'online'}`;
127
+ config.mqtt.online ??= true; // if not set, use topic "online"
128
+ config.mqtt.publish_bus_state ??= true; // if not set, use topic "bus_state"
129
+
130
+ mqttTopic = config.mqtt.topic ?? 'Vito';
131
+ function prefixMqttTopic(topic, defaultTopic) {
132
+ return `${mqttTopic}${mqttTopic.endsWith('/') || topic?.startsWith?.('/') ? '' : '/'}${
133
+ typeof topic === 'string' ? topic : defaultTopic}`;
134
+ }
135
+ mqttAvailabilityTopic = prefixMqttTopic(config.mqtt.online, 'online');
136
+
137
+ if (config.mqtt.publish_bus_state) {
138
+ const mqttBusStateTopic = prefixMqttTopic(config.mqtt.publish_bus_state, 'bus_state');
139
+ afterBusStateUpdate = async state => {
140
+ await mqttClient.publishAsync(
141
+ mqttBusStateTopic, `${state}`);
142
+ };
143
+ }
120
144
 
121
145
  const mqttOptions = {
122
146
  username: config.mqtt.username,
@@ -156,11 +180,11 @@ const packetQueue = async.queue(async task => {
156
180
  }
157
181
 
158
182
  if (task.direction === fromVitoToOpto && packet.start === 0x04) {
159
- busState = 0; // reset synchronization
183
+ await updateBusState(0); // reset synchronization
160
184
  } else if (busState === 0 && task.direction & toOpto && packet.start === 0x16 && 'zero' in packet) {
161
- busState = 1;
185
+ await updateBusState(1);
162
186
  } else if (busState === 1 && task.direction & fromOpto && packet.res === 0x06 && !packet.peek?.length) {
163
- busState = 2;
187
+ await updateBusState(2);
164
188
 
165
189
  logger.info('Synchronization completed. Streams are now flowing');
166
190
  }
@@ -205,7 +229,7 @@ const packetQueue = async.queue(async task => {
205
229
  const topic = mqttDpTopic(dp), value = dp ? dp.parse(packet.data) : packet.data;
206
230
  logger.debug(`Publishing ${dp ? dp.name : 'unknown'} data point (${formatAddr(packet.addr)}) to ${topic}:`, value);
207
231
  await mqttClient.publishAsync(topic, Buffer.isBuffer(value) ? bufferToFormat(value, config.buffer_format ?? 'hex') :
208
- `${ typeof value === 'number' ? parseFloat(value.toFixed(config.max_decimals ?? 4 )) : value }`);
232
+ `${ typeof value === 'number' ? parseFloat(value.toFixed(config.max_decimals ?? 4)) : value }`);
209
233
  }, 1 /* no concurrency, packets should to be processed in order */);
210
234
  packetQueue.error((err, task) => {
211
235
  logger.error(`Error while processing packet: ${task?.data?.toString('hex')}`, err);
@@ -236,6 +260,10 @@ const chunkQueue = async.queue(async task => {
236
260
  packetQueue.push({ data, packet, direction });
237
261
  } else {
238
262
  chunks.push(task.chunk);
263
+ if (chunks.length > (config.max_buffered_chunks ?? 100)) {
264
+ logger.warn(`The chunk queue has buffered more than ${config.max_buffered_chunks ?? 100} chunks in the same direction (${directionName(direction)}), which could indicate that the stream is stalled. Consider increasing the max_buffered_chunks setting in your config.toml file if you are polling huge packets or check if there are any issues with the serial connection.`);
265
+ chunks.shift(); // drop the oldest chunk
266
+ }
239
267
  }
240
268
  }, 1 /* no concurrency, chunks need to be processed in order */);
241
269
  chunkQueue.error((err, task) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "optolink-bridge",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Safely bridge the Optolink bus of your Viessmann heating system and publish attributes via MQTT + Home Assistant Device Discovery",
5
5
  "keywords": [
6
6
  "viessmann",
@@ -54,9 +54,9 @@
54
54
  "buffer-to-str": "^1.0.0",
55
55
  "change-case": "^5.4.4",
56
56
  "chokidar": "^5.0.0",
57
- "mqtt": "^5.14.1",
57
+ "mqtt": "^5.15.1",
58
58
  "serialport": "^13.0.0",
59
- "smol-toml": "^1.5.2",
59
+ "smol-toml": "^1.6.1",
60
60
  "yocto-queue": "^1.2.2"
61
61
  },
62
62
  "packageManager": "yarn@4.12.0"
package/serial.js CHANGED
@@ -56,11 +56,12 @@ export async function connect(vitoPath = '/dev/ttyS0', optoPath = '/dev/ttyUSB0'
56
56
  lock: true // exclusive
57
57
  };
58
58
 
59
- const vitoPort = new SerialPort({
59
+ // expose the vito + opto ports, mainly for debugging purpose
60
+ const vitoPort = eventEmitter.vitoPort = new SerialPort({
60
61
  path: vitoPath,
61
62
  ...portOptions
62
63
  });
63
- const optoPort = new SerialPort({
64
+ const optoPort = eventEmitter.optoPort = new SerialPort({
64
65
  path: optoPath,
65
66
  ...portOptions
66
67
  });
@@ -80,8 +81,8 @@ export async function connect(vitoPath = '/dev/ttyS0', optoPath = '/dev/ttyUSB0'
80
81
  };
81
82
 
82
83
  eventEmitter.pipeline = Promise.all([
83
- pipeline(vitoPort, createBridgeStream(fromVitoToOpto, vitoPort, optoPort), optoPort),
84
- pipeline(optoPort, createBridgeStream(fromOptoToVito, vitoPort, optoPort), vitoPort)
84
+ pipeline(vitoPort, eventEmitter.vitoToOptoBridge = createBridgeStream(fromVitoToOpto, vitoPort, optoPort), optoPort),
85
+ pipeline(optoPort, eventEmitter.optoToVitoBridge = createBridgeStream(fromOptoToVito, vitoPort, optoPort), vitoPort)
85
86
  ]);
86
87
 
87
88
  return eventEmitter;