balboa_worldwide_app 1.3.0 → 2.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.
- checksums.yaml +4 -4
- data/exe/bwa_client +41 -0
- data/exe/bwa_mqtt_bridge +394 -0
- data/{bin → exe}/bwa_proxy +3 -2
- data/{bin → exe}/bwa_server +3 -2
- data/lib/balboa_worldwide_app.rb +3 -1
- data/lib/bwa/client.rb +121 -91
- data/lib/bwa/crc.rb +3 -1
- data/lib/bwa/discovery.rb +18 -17
- data/lib/bwa/logger.rb +9 -7
- data/lib/bwa/message.rb +67 -53
- data/lib/bwa/messages/configuration.rb +3 -1
- data/lib/bwa/messages/configuration_request.rb +3 -1
- data/lib/bwa/messages/control_configuration.rb +12 -9
- data/lib/bwa/messages/control_configuration_request.rb +13 -11
- data/lib/bwa/messages/filter_cycles.rb +50 -22
- data/lib/bwa/messages/ready.rb +3 -1
- data/lib/bwa/messages/{set_temperature.rb → set_target_temperature.rb} +5 -3
- data/lib/bwa/messages/set_temperature_scale.rb +5 -3
- data/lib/bwa/messages/set_time.rb +4 -2
- data/lib/bwa/messages/status.rb +51 -44
- data/lib/bwa/messages/toggle_item.rb +29 -27
- data/lib/bwa/proxy.rb +17 -18
- data/lib/bwa/server.rb +16 -14
- data/lib/bwa/version.rb +3 -1
- metadata +70 -24
- data/bin/bwa_client +0 -43
- data/bin/bwa_mqtt_bridge +0 -614
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c2c576e6a37c43c681729ff2657eb07f9424be814bb1ecbb8a22639f3edb83b
|
4
|
+
data.tar.gz: d3c157c5a9c3776c66d061b15deec4b10d5c51ba4b4866e26ea79e703084e9a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f42ac7f5102742a13735b6661837611f179e2e8b8c6bbaa7c9938dc451ded4aa277f5314260cf0ee552b4422fc95c8fb669c6ba343f9eaee1216fbd10c4da574
|
7
|
+
data.tar.gz: 6a66e7afff3ce43eb0cc1bfb571f621ce3870547b82da5c0f82aea7f56d582b221e08f34967b1b766499406b36e25e8dc7028172ed8ad0f985d9ff7d310dd2f5
|
data/exe/bwa_client
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bwa/client"
|
5
|
+
require "bwa/discovery"
|
6
|
+
|
7
|
+
def watch(spa)
|
8
|
+
loop do
|
9
|
+
message = spa.poll
|
10
|
+
next if message.is_a?(BWA::Messages::Ready)
|
11
|
+
|
12
|
+
puts message.raw_data.unpack1("H*").scan(/[0-9a-f]{2}/).join(" ")
|
13
|
+
puts message.inspect
|
14
|
+
break if block_given? && yield
|
15
|
+
rescue BWA::InvalidMessage => e
|
16
|
+
puts e.message
|
17
|
+
puts e.raw_data.unpack1("H*").scan(/[0-9a-f]{2}/).join(" ")
|
18
|
+
break
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
if ARGV.empty?
|
23
|
+
spas = BWA::Discovery.discover
|
24
|
+
if spas.empty?
|
25
|
+
warn "Could not find spa!"
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
spa_ip = "tcp://#{spas.first.first}/"
|
29
|
+
else
|
30
|
+
spa_ip = ARGV[0]
|
31
|
+
end
|
32
|
+
|
33
|
+
spa = BWA::Client.new(spa_ip)
|
34
|
+
|
35
|
+
spa.request_configuration
|
36
|
+
spa.request_control_info
|
37
|
+
watch(spa) do
|
38
|
+
spa.last_status
|
39
|
+
end
|
40
|
+
|
41
|
+
watch(spa)
|
data/exe/bwa_mqtt_bridge
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sd_notify"
|
5
|
+
require "set"
|
6
|
+
require "json"
|
7
|
+
require "mqtt/home_assistant"
|
8
|
+
|
9
|
+
require "bwa/logger"
|
10
|
+
require "bwa/client"
|
11
|
+
require "bwa/discovery"
|
12
|
+
require "bwa/version"
|
13
|
+
|
14
|
+
class MQTTBridge
|
15
|
+
SIMPLE_PROPERTIES = %i[hold
|
16
|
+
priming
|
17
|
+
heating_mode
|
18
|
+
twenty_four_hour_time
|
19
|
+
heating
|
20
|
+
temperature_range
|
21
|
+
current_temperature
|
22
|
+
target_temperature].freeze
|
23
|
+
private_constant :SIMPLE_PROPERTIES
|
24
|
+
|
25
|
+
def initialize(mqtt_uri, bwa, device_id: "bwa", root_topic: "homie")
|
26
|
+
Thread.abort_on_exception = true
|
27
|
+
|
28
|
+
@homie = MQTT::Homie::Device.new(device_id, "BWA Link", mqtt: mqtt_uri, root_topic: root_topic)
|
29
|
+
@bwa = bwa
|
30
|
+
|
31
|
+
# spin until we have a full configuration
|
32
|
+
loop do
|
33
|
+
message = @bwa.poll
|
34
|
+
next if message.is_a?(BWA::Messages::Ready)
|
35
|
+
|
36
|
+
if message.is_a?(BWA::Messages::Status)
|
37
|
+
@bwa.request_control_info unless @bwa.control_configuration
|
38
|
+
@bwa.request_control_info2 unless @bwa.configuration
|
39
|
+
@bwa.request_filter_configuration unless @bwa.filter_cycles
|
40
|
+
end
|
41
|
+
|
42
|
+
break if @bwa.full_configuration?
|
43
|
+
end
|
44
|
+
|
45
|
+
@homie.home_assistant_device = {
|
46
|
+
manufacturer: "Balboa Water Group",
|
47
|
+
sw_version: BWA::VERSION,
|
48
|
+
model: @bwa.model
|
49
|
+
}
|
50
|
+
|
51
|
+
publish_basic_attributes
|
52
|
+
@homie.publish
|
53
|
+
|
54
|
+
# Tell systemd we've started up OK. Ignored if systemd not in use.
|
55
|
+
BWA.logger.warn "Balboa MQTT Bridge running (version #{BWA::VERSION})"
|
56
|
+
SdNotify.ready
|
57
|
+
|
58
|
+
loop do
|
59
|
+
message = @bwa.poll
|
60
|
+
next if message.is_a?(BWA::Messages::Ready)
|
61
|
+
|
62
|
+
case message
|
63
|
+
when BWA::Messages::FilterCycles
|
64
|
+
2.times do |i|
|
65
|
+
node = @homie["filter-cycle#{i + 1}"]
|
66
|
+
node["start-hour"].value = message.public_send(:"cycle#{i + 1}_start_hour")
|
67
|
+
node["start-minute"].value = message.public_send(:"cycle#{i + 1}_start_minute")
|
68
|
+
node["duration"].value = message.public_send(:"cycle#{i + 1}_duration")
|
69
|
+
node["enabled"].value = message.cycle2_enabled? if i == 1
|
70
|
+
end
|
71
|
+
when BWA::Messages::Status
|
72
|
+
# make sure time is in sync
|
73
|
+
now = Time.now
|
74
|
+
now_minutes = (now.hour * 60) + now.min
|
75
|
+
spa_minutes = (message.hour * 60) + message.minute
|
76
|
+
# check the difference in both directions
|
77
|
+
diff = [(spa_minutes - now_minutes) % 1440, 1440 - ((spa_minutes - now_minutes) % 1440)].min
|
78
|
+
|
79
|
+
# allow a skew of 1 minute, since the seconds will always be off
|
80
|
+
if diff > 1
|
81
|
+
spa_time_str = format("%02d:%02d", message.hour, message.minute)
|
82
|
+
now_str = format("%02d:%02d", now.hour, now.min)
|
83
|
+
BWA.logger.info "Spa time #{spa_time_str}, actually #{now_str}; correcting difference of #{diff} min"
|
84
|
+
@bwa.set_time(now.hour, now.min, twenty_four_hour_time: message.twenty_four_hour_time)
|
85
|
+
end
|
86
|
+
|
87
|
+
if @bwa.temperature_scale != @homie["spa"]["temperature-scale"].value
|
88
|
+
@homie.init do
|
89
|
+
@homie["spa"]["temperature-scale"].value = @bwa.temperature_scale
|
90
|
+
update_temperature_scale
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
SIMPLE_PROPERTIES.each do |prop|
|
95
|
+
property = @homie["spa"][prop.to_s.tr("_", "-")]
|
96
|
+
property.value = @bwa.public_send(prop)
|
97
|
+
end
|
98
|
+
2.times do |i|
|
99
|
+
@homie["filter-cycle#{i + 1}"]["running"].value = @bwa.status.filter_cycles[i]
|
100
|
+
end
|
101
|
+
|
102
|
+
@homie["spa"]["circulation-pump"].value = @bwa.circulation_pump if @bwa.configuration.circulation_pump
|
103
|
+
case @bwa.configuration.blower
|
104
|
+
when 0
|
105
|
+
# not present
|
106
|
+
when 1
|
107
|
+
@homie["spa"]["blower"].value = !@bwa.blower.zero?
|
108
|
+
else
|
109
|
+
@homie["spa"]["blower"].value = @bwa.blower
|
110
|
+
end
|
111
|
+
@homie["spa"]["mister"].value = @bwa.mister if @bwa.configuration.mister
|
112
|
+
|
113
|
+
@bwa.configuration.pumps.each_with_index do |speeds, i|
|
114
|
+
next if speeds.zero?
|
115
|
+
|
116
|
+
property = @homie["spa"]["pump#{i + 1}"]
|
117
|
+
property.value = speeds == 1 ? @bwa.pumps[i] != 0 : @bwa.pumps[i]
|
118
|
+
end
|
119
|
+
@bwa.configuration.lights.each_with_index do |exists, i|
|
120
|
+
next unless exists
|
121
|
+
|
122
|
+
@homie["spa"]["light#{i + 1}"].value = @bwa.lights[i]
|
123
|
+
end
|
124
|
+
@bwa.configuration.aux.each_with_index do |exists, i|
|
125
|
+
next unless exists
|
126
|
+
|
127
|
+
@homie["spa"]["aux#{i + 1}"].value = @bwa.lights[i]
|
128
|
+
end
|
129
|
+
|
130
|
+
# Tell systemd we are still alive and kicking. Ignored if systemd not in use.
|
131
|
+
SdNotify.watchdog
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def publish_basic_attributes
|
137
|
+
allow_toggle = lambda do |value|
|
138
|
+
next value if value == "toggle"
|
139
|
+
end
|
140
|
+
|
141
|
+
@homie.node("spa", "Hot Tub", @bwa.model) do |spa|
|
142
|
+
spa.property("hold",
|
143
|
+
"Hold",
|
144
|
+
:boolean,
|
145
|
+
@bwa.hold,
|
146
|
+
hass: { switch: { icon: "mdi:pause-octagon" } },
|
147
|
+
non_standard_value_check: allow_toggle) do |value|
|
148
|
+
next @bwa.toggle_hold if value == "toggle"
|
149
|
+
|
150
|
+
@bwa.hold = value
|
151
|
+
end
|
152
|
+
spa.property("priming", "Priming", :boolean, @bwa.priming, hass: { binary_sensor: { icon: "mdi:fast-forward" } })
|
153
|
+
spa.property("heating-mode",
|
154
|
+
"Heating Mode",
|
155
|
+
:enum,
|
156
|
+
@bwa.heating_mode,
|
157
|
+
format: BWA::Client::HEATING_MODES,
|
158
|
+
hass: { select: { icon: "mdi:cog-play" } },
|
159
|
+
non_standard_value_check: allow_toggle) do |value|
|
160
|
+
next @bwa.toggle_heating_mode if value == "toggle"
|
161
|
+
|
162
|
+
@bwa.heating_mode = value.to_sym
|
163
|
+
end
|
164
|
+
spa.property("temperature-scale",
|
165
|
+
"Temperature Scale",
|
166
|
+
:enum,
|
167
|
+
@bwa.temperature_scale,
|
168
|
+
format: %w[fahrenheit celsius],
|
169
|
+
hass: :select) do |value|
|
170
|
+
@bwa.temperature_scale = value.to_sym
|
171
|
+
end
|
172
|
+
spa.property("twenty-four-hour-time",
|
173
|
+
"24 Hour Time",
|
174
|
+
:boolean,
|
175
|
+
@bwa.twenty_four_hour_time?,
|
176
|
+
hass: { switch: { icon: "mdi:timer-cog" } }) do |value|
|
177
|
+
now = Time.now
|
178
|
+
@bwa.set_time(now.hour, now.min, twenty_four_hour_time: value)
|
179
|
+
end
|
180
|
+
spa.property("heating",
|
181
|
+
"Heating",
|
182
|
+
:boolean,
|
183
|
+
@bwa.heating?,
|
184
|
+
hass: {
|
185
|
+
binary_sensor: { device_class: :running, icon: "mdi:hot-tub" }
|
186
|
+
})
|
187
|
+
spa.property("temperature-range",
|
188
|
+
"Temperature Range",
|
189
|
+
:enum,
|
190
|
+
@bwa.temperature_range,
|
191
|
+
format: %i[high low],
|
192
|
+
hass: { select: { icon: "mdi:thermometer-lines" } },
|
193
|
+
non_standard_value_check: allow_toggle) do |value|
|
194
|
+
next @bwa.toggle_temperature_range if value == "toggle"
|
195
|
+
|
196
|
+
@bwa.temperature_range = value.to_sym
|
197
|
+
end
|
198
|
+
spa.property("current-temperature", "Current Water Temperature", :float, @bwa.current_temperature)
|
199
|
+
spa.property("target-temperature", "Target Water Temperature", :float, @bwa.target_temperature) do |value|
|
200
|
+
@bwa.target_temperature = value
|
201
|
+
end
|
202
|
+
update_temperature_scale
|
203
|
+
|
204
|
+
unless @bwa.configuration.blower.zero?
|
205
|
+
if @bwa.configuration.blower == 1
|
206
|
+
args = [:boolean, !@bwa.blower.zero?]
|
207
|
+
kwargs = { hass: { switch: { icon: "mdi:chart-bubble" } } }
|
208
|
+
else
|
209
|
+
args = [:integer, @bwa.blower]
|
210
|
+
kwargs = {
|
211
|
+
format: 0..@bwa.configuration.blower,
|
212
|
+
hass: { number: { icon: "mdi:chart-bubble" } }
|
213
|
+
}
|
214
|
+
end
|
215
|
+
|
216
|
+
spa.property("blower",
|
217
|
+
"Blower",
|
218
|
+
*args,
|
219
|
+
non_standard_value_check: allow_toggle,
|
220
|
+
**kwargs) do |value|
|
221
|
+
next @bwa.toggle_blower if value == "toggle"
|
222
|
+
|
223
|
+
@bwa.blower = value
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
if @bwa.configuration.mister
|
228
|
+
spa.property("mister",
|
229
|
+
"Mister",
|
230
|
+
:boolean,
|
231
|
+
@bwa.mister,
|
232
|
+
hass: { switch: { icon: "mdi:sprinkler-fire" } },
|
233
|
+
non_standard_value_check: allow_toggle) do |value|
|
234
|
+
next @bwa.toggle_mister if value == "toggle"
|
235
|
+
|
236
|
+
@bwa.mister = value
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
if @bwa.configuration.circulation_pump
|
241
|
+
spa.property("circulation-pump",
|
242
|
+
"Circulation Pump Running",
|
243
|
+
:boolean,
|
244
|
+
@bwa.circulation_pump,
|
245
|
+
hass: { binary_sensor: { device_class: :running, icon: "mdi:sync" } })
|
246
|
+
end
|
247
|
+
|
248
|
+
single_pump = @bwa.configuration.pumps.count { |speeds| !speeds.zero? } == 1
|
249
|
+
@bwa.configuration.pumps.each_with_index do |speeds, i|
|
250
|
+
next if speeds.zero?
|
251
|
+
|
252
|
+
if speeds == 1
|
253
|
+
args = [:boolean, !@bwa.pumps[i].zero?]
|
254
|
+
kwargs = { hass: { switch: { icon: "mdi:chart-bubble" } } }
|
255
|
+
else
|
256
|
+
args = [:integer, @bwa.pumps[i]]
|
257
|
+
kwargs = { format: 0..speeds,
|
258
|
+
hass: { number: { icon: "mdi:chart-bubble" } } }
|
259
|
+
end
|
260
|
+
name = single_pump ? "Pump" : "Pump #{i + 1}"
|
261
|
+
spa.property("pump#{i + 1}",
|
262
|
+
name,
|
263
|
+
*args,
|
264
|
+
non_standard_value_check: allow_toggle,
|
265
|
+
**kwargs) do |value|
|
266
|
+
next @bwa.toggle_pump(i) if value == "toggle"
|
267
|
+
|
268
|
+
@bwa.set_pump(i, value)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
single_light = @bwa.configuration.lights.count(&:itself)
|
273
|
+
@bwa.configuration.lights.each_with_index do |exists, i|
|
274
|
+
next unless exists
|
275
|
+
|
276
|
+
name = single_light ? "Lights" : "Lights #{i + 1}"
|
277
|
+
spa.property("light#{i + 1}",
|
278
|
+
name,
|
279
|
+
:boolean,
|
280
|
+
@bwa.lights[i],
|
281
|
+
hass: { light: { icon: "mdi:car-parking-lights" } },
|
282
|
+
non_standard_value_check: allow_toggle) do |value|
|
283
|
+
next @bwa.toggle_light(i) if value == "toggle"
|
284
|
+
|
285
|
+
@bwa.set_light(i, value)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
@bwa.configuration.aux.each_with_index do |exists, i|
|
290
|
+
next unless exists
|
291
|
+
|
292
|
+
spa.property("aux#{i + 1}",
|
293
|
+
"Auxiliary #{i + 1}",
|
294
|
+
:boolean,
|
295
|
+
@bwa.aux[i],
|
296
|
+
hass: :switch,
|
297
|
+
non_standard_value_check: allow_toggle) do |value|
|
298
|
+
next @bwa.toggle_aux(i) if value == "toggle"
|
299
|
+
|
300
|
+
@bwa.set_aux(i, value)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
2.times do |i|
|
306
|
+
@homie.node("filter-cycle#{i + 1}", "Filter Cycle #{i + 1}", "Filter Cycle") do |cycle|
|
307
|
+
cycle.property("running",
|
308
|
+
"Running",
|
309
|
+
:boolean,
|
310
|
+
@bwa.status.filter_cycles[i],
|
311
|
+
hass: { binary_sensor: { icon: "mdi:air-filter" } })
|
312
|
+
cycle.property("start-hour",
|
313
|
+
"Start Hour",
|
314
|
+
:integer,
|
315
|
+
@bwa.filter_cycles.public_send(:"cycle#{i + 1}_start_hour"),
|
316
|
+
format: 0...24,
|
317
|
+
unit: "hours",
|
318
|
+
hass: { number: { icon: "mdi:clock" } }) do |value|
|
319
|
+
update_filter_cycles(:"cycle#{i + 1}_start_hour", value)
|
320
|
+
end
|
321
|
+
cycle.property("start-minute",
|
322
|
+
"Start Minute",
|
323
|
+
:integer,
|
324
|
+
@bwa.filter_cycles.public_send(:"cycle#{i + 1}_start_minute"),
|
325
|
+
format: 0...60,
|
326
|
+
unit: "minutes",
|
327
|
+
hass: { number: { icon: "mdi:clock" } }) do |value|
|
328
|
+
update_filter_cycles(:"cycle#{i + 1}_start_minute", value)
|
329
|
+
end
|
330
|
+
cycle.property("duration",
|
331
|
+
"Duration",
|
332
|
+
:integer,
|
333
|
+
@bwa.filter_cycles.public_send(:"cycle#{i + 1}_duration"),
|
334
|
+
format: 0...1440,
|
335
|
+
unit: "minutes",
|
336
|
+
hass: { number: { icon: "mdi:clock" } }) do |value|
|
337
|
+
update_filter_cycles(:"cycle#{i + 1}_duration", value)
|
338
|
+
end
|
339
|
+
|
340
|
+
next unless i == 1
|
341
|
+
|
342
|
+
cycle.property("enabled",
|
343
|
+
"Enabled",
|
344
|
+
:boolean,
|
345
|
+
hass: { switch: { icon: "mdi:filter-check" } }) do |value|
|
346
|
+
update_filter_cycles(:cycle2_enabled, value)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def update_filter_cycles(prop, value)
|
353
|
+
new_config = @bwa.filter_cycles.dup
|
354
|
+
new_config.public_send("#{prop}=", value)
|
355
|
+
@bwa.update_filter_cycles(new_config)
|
356
|
+
end
|
357
|
+
|
358
|
+
def update_temperature_scale
|
359
|
+
@homie["spa"]["current-temperature"].unit =
|
360
|
+
@homie["spa"]["target-temperature"].unit =
|
361
|
+
"°#{@bwa.temperature_scale.to_s[0].upcase}"
|
362
|
+
if @bwa.temperature_scale == :celsius
|
363
|
+
@homie["spa"]["current-temperature"].format = 0..42
|
364
|
+
@homie["spa"]["target-temperature"].format = 10..40
|
365
|
+
else
|
366
|
+
@homie["spa"]["current-temperature"].format = 32..108
|
367
|
+
@homie["spa"]["target-temperature"].format = 50..106
|
368
|
+
end
|
369
|
+
|
370
|
+
@homie["spa"]["current-temperature"].hass_sensor(device_class: :temperature)
|
371
|
+
@homie["spa"]["target-temperature"].hass_number(icon: "mdi:thermometer")
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
mqtt_uri = ARGV.shift
|
376
|
+
|
377
|
+
if ARGV.empty?
|
378
|
+
spas = BWA::Discovery.discover
|
379
|
+
if spas.empty?
|
380
|
+
BWA.logger.fatal "Could not find spa!"
|
381
|
+
warn "Could not find spa!"
|
382
|
+
exit 1
|
383
|
+
end
|
384
|
+
spa_ip = "tcp://#{spas.first.first}/"
|
385
|
+
else
|
386
|
+
spa_ip = ARGV[0]
|
387
|
+
end
|
388
|
+
|
389
|
+
spa = BWA::Client.new(spa_ip)
|
390
|
+
|
391
|
+
spa.request_configuration
|
392
|
+
spa.request_filter_configuration
|
393
|
+
|
394
|
+
MQTTBridge.new(mqtt_uri, spa)
|
data/{bin → exe}/bwa_proxy
RENAMED
data/{bin → exe}/bwa_server
RENAMED
data/lib/balboa_worldwide_app.rb
CHANGED