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 +12 -0
- package/config.toml +7 -0
- package/index.js +36 -8
- package/package.json +3 -3
- package/serial.js +5 -4
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
183
|
+
await updateBusState(0); // reset synchronization
|
|
160
184
|
} else if (busState === 0 && task.direction & toOpto && packet.start === 0x16 && 'zero' in packet) {
|
|
161
|
-
|
|
185
|
+
await updateBusState(1);
|
|
162
186
|
} else if (busState === 1 && task.direction & fromOpto && packet.res === 0x06 && !packet.peek?.length) {
|
|
163
|
-
|
|
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
|
|
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.
|
|
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.
|
|
57
|
+
"mqtt": "^5.15.1",
|
|
58
58
|
"serialport": "^13.0.0",
|
|
59
|
-
"smol-toml": "^1.
|
|
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
|
-
|
|
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;
|