optolink-bridge 1.0.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/config.toml ADDED
@@ -0,0 +1,292 @@
1
+ # serial port of the Vitoconnect device, defaults to "/dev/ttyS0", Raspberry 3B+ (and some others) use "/dev/ttyAMA0"
2
+ port_vito = "/dev/ttyS0"
3
+ # serial port of the Optolink adapter / device, defaults to "/dev/ttyUSB0"
4
+ port_opto = "/dev/ttyUSB0"
5
+
6
+ # log level, one of "none", "error", "warn", "info", "trace" or "verbose" defaults to "warn"
7
+ # note: this setting may be changed during runtime and is picked up from this file automatically
8
+ log_level = "warn"
9
+
10
+ # data points to listen to and publish
11
+ # (use tools/convert_poll_items.js to convert your existing optolink-splitter poll_list to data_points)
12
+ data_points = [
13
+ # [name, addr, [type], [scale]]
14
+ # name: name of the data point, as published
15
+ # addr: address of the data point, either as hexadecimal 0xab01 or as decimal 43873
16
+ # type:
17
+ # "raw" for raw data (0x..., alias for buffer)
18
+ # "int" / "uint" for integer values matching the size of the data (e.g. (u)int16 for 2 bytes)
19
+ # corner case: 3 byte values are mapped to (u)int16 and 5 byte values to (u)int32
20
+ # due to a many numerical data points having a trailing status byte that is ignored
21
+ # "byte" for a single byte value (alias for uint8)
22
+ # "vdatetime" for Viessmann specific date and time values
23
+ # "unixtime" for unix timestamps
24
+ # "utf8" for utf8 encoded strings
25
+ # or any data types supported by binary-parser (e.g. uint16)
26
+ # note that byte-bit filters are *not* yet supported
27
+ # scale: a factor to scale the numeric value by, e.g. 0.1 to convert 100 to 10.0
28
+ # if type is set to a number, the data point is treated as a integer matching the size of the data (e.g. (u)int16
29
+ # for 2 bytes), the number represents the scale, scale can the be set to true, to indicate a signed number
30
+ # if no type is given, the data point is treated as a raw
31
+
32
+ ["outside_temperature", 0x01C1, "int16", 0.1], # 3 bytes (sensor temp. w/ status), 2 bytes as 0x0101, but not sent by Vitoconnect
33
+ ["averaged_outside_temperature", 0x160D, "int16", 0.1],
34
+ ["averaging_time_outside_temperature", 0x7002, "int16"],
35
+
36
+ ["operating_mode", 0xB000, "byte"],
37
+
38
+ ["heating_circuit_circulation_pump", 0x048D, "byte"],
39
+ ["secondary_pump", 0x0484, "byte"],
40
+
41
+ ["hkl_level", 0x2006, "int16", 0.1],
42
+ ["hkl_slope", 0x2007, "int16", 0.1],
43
+ ["hysteresis", 0x6007, "int16", 0.1], # also as 0x7203, but not sent by Vitoconnect
44
+
45
+ ["buffer_temperature", 0x01CB, "int16", 0.1], # 3 bytes (sensor temp. w/ status), 2 bytes as 0x010B, but not sent by Vitoconnect
46
+
47
+ ["primary_circuit_flow_temperature", 0xb400, "int16", 0.1], # 3 bytes (sensor temp. w/ status), 2 bytes as 0x0103, but not sent by Vitoconnect
48
+ ["secondary_circuit_flow_temperature", 0xb402, "int16", 0.1], # 3 bytes (sensor temp. w/ status), 2 bytes as 0x0105, but not sent by Vitoconnect
49
+ ["return_temperature", 0x01c6, "int16", 0.1], # 3 bytes (sensor temp. w/ status), 2 bytes as 0x0106, but not sent by Vitoconnect
50
+
51
+ ["3way_valve_position", 0x0494, "byte"],
52
+
53
+ ["suction_gas_temperature", 0xb409, "int16", 0.1],
54
+ ["suction_gas_pressure", 0xb410, "int16", 0.1],
55
+ ["hot_gas_temperature", 0xb40a, "int16", 0.1],
56
+ ["hot_gas_pressure", 0xb411, "int16", 0.1],
57
+ ["liquid_gas_temperature", 0xb404, "int16", 0.1],
58
+ ["condensation_temperature", 0xb408, "int16", 0.1],
59
+ ["evaporation_temperature", 0xb407, "int16", 0.1],
60
+ ["compressor_power", 0xb423, "uint8"],
61
+ ["ecv_position", 0xb424, "uint8"],
62
+
63
+ ["dhw_temperature_storage", 0x01CD, "int16", 0.1], # 3 bytes (sensor temp. w/ status), 2 bytes as 0x010D, but not sent by Vitoconnect
64
+ ["dhw_temperature_setpoint", 0x6000, "int16", 0.1],
65
+ ["dhw_temperature_setpoint2", 0x600C, "int16", 0.1],
66
+ ["dhw_circulation_pump", 0x0490, "byte"],
67
+
68
+ #["room_temperature_setpoint_actual", 0x0116, "int16", 0.1],
69
+ #["room_temperature_setpoint_set", 0x011B, "int16", 0.1],
70
+ ["room_temperature_setpoint_normal", 0x2000, "int16", 0.1],
71
+ ["room_temperature_setpoint_reduced", 0x2001, "int16", 0.1],
72
+ ["room_temperature_setpoint_party", 0x2022, "int16", 0.1],
73
+
74
+ ["compressor_phase", 0x0E1A, "byte"], # V1AppState, undocumented 0x130B is used by Vitoconnect
75
+ ["compressor_starts", 0x0500, "int32"],
76
+ ["compressor_operating_hours", 0x0580, "uint32", 0.0002777777777777778],
77
+ ["compressor_heating_power", 0x16A0, "uint"],
78
+ ["compressor_power_consumption", 0x16A4, "uint"],
79
+
80
+ ["one_time_dhw", 0xB020, "byte"],
81
+
82
+ ["energy_balance_factor", 0x163F, "int8"],
83
+ ["energy_balance_heating", 0x1640, "uint32"],
84
+ ["energy_balance_dhw", 0x1650, "uint32"],
85
+ ["energy_balance_heating_electric", 0x1660, "uint32"],
86
+ ["energy_balance_dhw_electric", 0x1670, "uint32"],
87
+
88
+ ["annual_performance_factor", 0x1680, "int8", 0.1],
89
+ ["annual_performance_factor_heating", 0x1681, "int8", 0.1],
90
+ ["annual_performance_factor_dhw", 0x1682, "int8", 0.1],
91
+
92
+ ["auxiliary_heater_enable", 0x6014, "byte"],
93
+ ["electric_heater_enable", 0x6015, "byte"]
94
+ ]
95
+
96
+ # if set (and not empty), the items in this list are polled at the specified intervals
97
+ # note: setting a poll_items list, results in optolink-bridge being started in "intercept" mode
98
+ # if not set, a PassThrough stream will be used to forward the data from the Optolink bus, if set
99
+ # a Transform stream will be used instead, to inject the poll items into the communication
100
+ poll_items = [
101
+ # [ival, addr, dlen]
102
+ # ival: the interval in seconds to poll the item
103
+ # warning: the Optolink serial port is using quite a low baud rate of 4800 bauds / bytes, which
104
+ # (with parity bits included, and assuming the average packet size) results in about 20-25 packets
105
+ # that are transmitted via the bus each second. this includes requests and responses, meaning that only
106
+ # about 10-12 requests can be sent per second before the bus becomes saturated. optolink-bridge will
107
+ # only inject one poll request per message sent to the Optolink bus, meaning the maximum polling rate of
108
+ # items is about 5-6 items per second, reducing the Optolink bus throughput in half. thus the intervals
109
+ # in the poll_items list should be set accordingly. say you have 10 items that you each want to poll every
110
+ # second, the polling queue will become over saturated quickly and a warning will be logged to console
111
+ # addr: the address to poll
112
+ # dlen: the length of data to poll, note that your heating device will likely respond with a NAK / ERR, in
113
+ # in case the length of the data is not specified as the address / data type defines it
114
+ [15, 0xb408, 3], # condensation_temperature
115
+ [15, 0xb407, 3] # evaporation_temperature
116
+ ]
117
+
118
+ # the format to publish raw / byte / buffer data, defaults to "hex", one of:
119
+ # "id" or "identity" (publish binary data to MQTT)
120
+ # "hex" or "hex_upper" (lower/upper case hex string)
121
+ # "0x" or "0x_upper" (lower/upper case hex string with 0x prefix)
122
+ # "base64" (base64 encoded string)
123
+ buffer_format = "hex"
124
+ # the maximum number of decimals to round to on publish, defaults to 4
125
+ max_decimals = 4
126
+
127
+ # automatically reload data points and poll items from this configuration file if file changes are detected,
128
+ # without (!) interrupting the active Vitoconnect / Optolink communication, defaults to true
129
+ # note: this setting only affects reloading data points and poll list items, other changes might not be picked up
130
+ # also poll_items will only refresh, in case the list was populated, when you started optolink-bridge
131
+ # in case you add a item to the (previously empty) poll_items list, you will need to restart optolink-bridge
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
+ auto_reload_addr_items = true
134
+
135
+ [mqtt]
136
+
137
+ # broker protocol / host / port
138
+ url = "mqtt://127.0.0.1:1883"
139
+ # broker username
140
+ username = "user"
141
+ # broker password
142
+ password = "password"
143
+ # base topic to publish attributes to, defaults to "Vito"
144
+ # e.g.: Vito/<dpname>
145
+ topic = "Vito"
146
+ # suffix to append to the topic, defaults to <dpname>
147
+ # allowed wildcards are <addr> (or <dpaddr> as alias) and <dpname>
148
+ suffix = "<dpname>"
149
+ # retain a "online" topic, e.g. Vito/online, defaults to true / "online", set to a string to use a different topic
150
+ # note: if set to false, home assistant will not be able to determine if the devices / entities are available
151
+ online = true
152
+
153
+ # publish unknown data points to MQTT as raw data
154
+ publish_unknown_dps = false
155
+ # the suffix to use to publish unknown data points, defaults to "raw/<addr>"
156
+ unknown_dp_suffix = "raw/<addr>"
157
+
158
+ [mqtt.options]
159
+ # other broker options (see https://github.com/mqttjs/MQTT.js?tab=readme-ov-file#client options)
160
+ #...
161
+
162
+ [mqtt.device_discovery]
163
+
164
+ # set to false to disable publishing a device discovery payload
165
+ enabled = true
166
+
167
+ # the discovery prefix for devices, defaults to "homeassistant"
168
+ prefix = "homeassistant"
169
+
170
+ # optolink-bridge publishes a single device discovery payload, see:
171
+ # https://www.home-assistant.io/integrations/mqtt/#device-discovery-payload
172
+
173
+ # by default all data_points will be published as basic sensors, you may use the next section(s) to override specific
174
+ # settings of the discovery payload, for example to set device classes, icons, units, etc., see the home assistant
175
+ # mqtt integration documentation for details. note that all overrides must be specified in their non-abbreviated form
176
+
177
+ [mqtt.device_discovery.overrides]
178
+
179
+ [mqtt.device_discovery.overrides.device]
180
+
181
+ # if not set, serial_number and identifiers default to the UTF8 string representation of 0xF010 (device serial number).
182
+ # in case no identifiers are set and no serial_number is sent by Vitoconnect, no device discovery payload will be published!
183
+
184
+ # identifiers = "any_identifier_defaults_to_serial_number"
185
+ # serial_number = "device_serial_number"
186
+ # name = "Vito"
187
+ # model = "Vito"
188
+ # manufacturer = "Viessmann"
189
+
190
+ # the following override sections will override the default sensor and binary_sensor entity attributes, as listed here:
191
+ # https://www.home-assistant.io/integrations/sensor.mqtt/ and https://www.home-assistant.io/integrations/binary_sensor.mqtt/
192
+ # by default optolink-bridge publishes all data_points as sensors. in case you do not wish to publish a specific data_point,
193
+ # you may set its entry in the overrides section to false, e.g.: some_data_point = false
194
+ [mqtt.device_discovery.overrides.sensor]
195
+
196
+ outside_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1, icon = "mdi:thermometer" }
197
+ averaged_outside_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1, icon = "mdi:thermometer" }
198
+ averaging_time_outside_temperature = { device_class = "duration", unit_of_measurement = "min" }
199
+
200
+ operating_mode = { icon = "mdi:cog", value_template = """
201
+ {% set mapping = ({
202
+ '00': 'Shutdown',
203
+ '01': 'Hot Water Only',
204
+ '02': 'Heating and Hot Water',
205
+ '03': 'Mode 3',
206
+ '04': 'Continuously Reduced',
207
+ '05': 'Continuously Normal',
208
+ '06': 'Normal Shutdown',
209
+ '07': 'Cooling Only'
210
+ }) %}
211
+ {{ mapping[value] | default('Unknown') }}""" }
212
+
213
+ hkl_level = { state_class = "measurement", icon = "mdi:chart-line" }
214
+ hkl_slope = { state_class = "measurement", icon = "mdi:chart-line" }
215
+ hysteresis = { device_class = "temperature", unit_of_measurement = "K", icon = "mdi:thermometer-lines" }
216
+
217
+ buffer_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
218
+ primary_circuit_flow_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
219
+ secondary_circuit_flow_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
220
+ return_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
221
+
222
+ "3way_valve_position" = { unit_of_measurement = "%", suggested_display_precision = 0, icon = "mdi:valve", value_template = "{{ value | float * 100 }}" }
223
+
224
+ suction_gas_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
225
+ suction_gas_pressure = { device_class = "pressure", unit_of_measurement = "bar", suggested_display_precision = 1 }
226
+ hot_gas_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
227
+ hot_gas_pressure = { device_class = "pressure", unit_of_measurement = "bar", suggested_display_precision = 1 }
228
+ liquid_gas_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
229
+ condensation_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
230
+ evaporation_temperature = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
231
+ compressor_power = { unit_of_measurement = "%", suggested_display_precision = 0, icon = "mdi:heat-pump", value_template = "{{ value | int }}" }
232
+ ecv_position = { unit_of_measurement = "%", suggested_display_precision = 0, icon = "mdi:valve", value_template = "{{ value | int }}" }
233
+
234
+ dhw_temperature_storage = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
235
+ dhw_temperature_setpoint = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
236
+ dhw_temperature_setpoint2 = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
237
+
238
+ room_temperature_setpoint_normal = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
239
+ room_temperature_setpoint_reduced = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
240
+ room_temperature_setpoint_party = { device_class = "temperature", unit_of_measurement = "°C", suggested_display_precision = 1 }
241
+
242
+ compressor_phase = { icon = "mdi:information", value_template = """
243
+ {% set mapping = ({
244
+ '00': 'Off',
245
+ '01': 'Cooling',
246
+ '02': 'Heating',
247
+ '03': 'Error',
248
+ '04': 'Switching to Cooling',
249
+ '05': 'Defrosting',
250
+ '06': 'Waiting',
251
+ '07': 'Standby',
252
+ '08': 'Switching to Heating',
253
+ '09': 'Stopping',
254
+ '0A': 'Manual Operation',
255
+ '0B': 'Starting',
256
+ '0C': 'Utility Lock',
257
+ '0D': 'Pre-Start',
258
+ '0E': 'Post-Stop',
259
+ '0F': 'Blocked',
260
+ '10': 'Pump-Down',
261
+ '11': 'PD -> Comp. start',
262
+ '12': 'PD -> pressure reached',
263
+ '13': 'PD -> shutdown'
264
+ }) %}
265
+ {{ mapping[value[:2]] | default('Unknown') }}""" }
266
+
267
+ compressor_starts = { unit_of_measurement = "cycles", state_class = "total_increasing", icon = "mdi:restart" }
268
+ compressor_operating_hours = { device_class = "duration", unit_of_measurement = "h", state_class = "total_increasing", icon = "mdi:fan-clock" }
269
+ compressor_heating_power = { device_class = "power", unit_of_measurement = "W" }
270
+ compressor_power_consumption = { device_class = "power", unit_of_measurement = "W" }
271
+
272
+ annual_performance_factor = { state_class = "measurement", icon = "mdi:chart-line", value_template = "{{ value | float | round(2) }}" }
273
+ annual_performance_factor_heating = { state_class = "measurement", icon = "mdi:chart-line", value_template = "{{ value | float | round(2) }}" }
274
+ annual_performance_factor_dhw = { state_class = "measurement", icon = "mdi:chart-line", value_template = "{{ value | float | round(2) }}" }
275
+
276
+ energy_balance_factor = { device_class = "power_factor", state_class = "measurement", value_template = "{{ (value | float / 10) | round(2) }}" }
277
+ energy_balance_heating = { device_class = "energy", unit_of_measurement = "kWh", state_class = "total_increasing", icon = "mdi:fire", value_template = "{{ (value | float * states('sensor.energy_balance_factor') | float) | round(2) }}" }
278
+ energy_balance_dhw = { device_class = "energy", unit_of_measurement = "kWh", state_class = "total_increasing", icon = "mdi:water", value_template = "{{ (value | float * states('sensor.energy_balance_factor') | float) | round(2) }}" }
279
+ energy_balance_heating_electric = { device_class = "energy", unit_of_measurement = "kWh", state_class = "total_increasing", icon = "mdi:flash", value_template = "{{ (value | float * states('sensor.energy_balance_factor') | float) | round(2) }}" }
280
+ energy_balance_dhw_electric = { device_class = "energy", unit_of_measurement = "kWh", state_class = "total_increasing", icon = "mdi:flash", value_template = "{{ (value | float * states('sensor.energy_balance_factor') | float) | round(2) }}" }
281
+
282
+ # by default optolink-bridge publishes all data_points as sensors, in case you want to publish specific data_points as
283
+ # binary sensors, you may specify them in the next section. in case you do not want to change any attribute of the
284
+ # binary sensor, you may just specify the data point name with a value of true, e.g.: heating_circuit_circulation_pump = true
285
+ [mqtt.device_discovery.overrides.binary_sensor]
286
+
287
+ heating_circuit_circulation_pump = { payload_on = "01", payload_off = "00", device_class = "running", icon = "mdi:pump" }
288
+ secondary_pump = { payload_on = "01", payload_off = "00", device_class = "running", icon = "mdi:pump" }
289
+ dhw_circulation_pump = { payload_on = "01", payload_off = "00", device_class = "running", icon = "mdi:pump" }
290
+ one_time_dhw = { payload_on = "02", payload_off = "00", device_class = "heat", icon = "mdi:water-thermometer" }
291
+ auxiliary_heater_enable = { payload_on = "01", payload_off = "00", device_class = "running", icon = "mdi:heating-coil" }
292
+ electric_heater_enable = { payload_on = "01", payload_off = "00", device_class = "running", icon = "mdi:heating-coil" }
package/discovery.js ADDED
@@ -0,0 +1,74 @@
1
+ import packageJson from './package.json' with { type: 'json' };
2
+ import { capitalCase } from 'change-case';
3
+
4
+ export async function publishDevice({
5
+ mqttClient, mqttAvailabilityTopic, mqttDpTopic,
6
+ deviceSerialNo, prefix = 'homeassistant', dps, overrides = {}
7
+ }) {
8
+ if (!deviceSerialNo) {
9
+ throw new TypeError('Device serial number must be provided');
10
+ }
11
+
12
+ function discoveryComponent(dp) {
13
+ const unique_id = `vito_${deviceSerialNo}_${dp.name}`;
14
+
15
+ // check if there are any overrides for this data point
16
+ let platform = 'sensor', override;
17
+ for (const overridePlatform of Object.keys(overrides)) {
18
+ if (overridePlatform === 'device') { continue; }
19
+ if (override = overrides[overridePlatform]?.[dp.name]) {
20
+ platform = overridePlatform; // the override determines the platform of the data point
21
+ break;
22
+ }
23
+ }
24
+
25
+ if (override === false) {
26
+ return {}; // do not include this data point in device discovery
27
+ } else if (override === true) {
28
+ override = {}; // include this data point with default settings in the specified platform
29
+ }
30
+
31
+ return {
32
+ [unique_id]: {
33
+ platform,
34
+
35
+ unique_id,
36
+ name: capitalCase(dp.name), // will automatically get prefixed with the device name
37
+ state_topic: mqttDpTopic(dp),
38
+
39
+ ...(override ?? {})
40
+ }
41
+ };
42
+ }
43
+
44
+ // build the discovery record
45
+ const discoveryPayload = {
46
+ device: {
47
+ identifiers: deviceSerialNo,
48
+ serial_number: deviceSerialNo,
49
+ name: 'Vito', model: `Vito`,
50
+ manufacturer: 'Viessmann',
51
+ ...(overrides?.device ?? {})
52
+ },
53
+ origin: { // see https://www.home-assistant.io/integrations/mqtt/#adding-information-about-the-origin-of-a-discovery-message
54
+ name: 'optolink-bridge',
55
+ sw_version: packageJson.version,
56
+ support_url: 'https://github.com/kristian/optolink-bridge/issues'
57
+ },
58
+ ...(mqttAvailabilityTopic ? {
59
+ availability: [ // see https://www.home-assistant.io/integrations/mqtt/#using-availability-topics
60
+ {
61
+ topic: mqttAvailabilityTopic,
62
+ payload_available: 'true',
63
+ payload_not_available: 'false',
64
+ }
65
+ ]
66
+ } : {}),
67
+ components: Object.assign({}, ...[...(dps?.values() ?? [])].map(discoveryComponent))
68
+ };
69
+
70
+ // publish to device discovery topic, see: https://www.home-assistant.io/integrations/mqtt/#discovery-messages
71
+ // config messages are retained, see: https://www.home-assistant.io/integrations/mqtt/#using-retained-config-messages
72
+ await mqttClient.publishAsync(`${prefix ?? 'homeassistant'}/device/vito_${deviceSerialNo}/config`,
73
+ JSON.stringify(discoveryPayload), { retain: true });
74
+ }
Binary file
Binary file
package/docs/logo.png ADDED
Binary file
Binary file
Binary file
Binary file