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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1f61d30556c0536efad6d27c229cefc0bccd1293a4dc491230a3e68cfddca13
4
- data.tar.gz: 3014c3c575a53cceaa2f9c2903b1977480bffa5cc0db5e44f08bce8fc8671e17
3
+ metadata.gz: 6c2c576e6a37c43c681729ff2657eb07f9424be814bb1ecbb8a22639f3edb83b
4
+ data.tar.gz: d3c157c5a9c3776c66d061b15deec4b10d5c51ba4b4866e26ea79e703084e9a8
5
5
  SHA512:
6
- metadata.gz: 3ff3043cbbdefd778ee2fbea74ce46ad8c73860f8e541c9035911219b6d6048203f817a2f129bac16678334d4f02cddea806814357d13710c4c76fc3e1987b5e
7
- data.tar.gz: bb1d5468380c06c7bc38c34cf984cd9c4550fb3a3f1314f1b85617df0cff003ece26ce4603746f59f0e00950ae8244aaf0f477149f3b693ab67b8f6ff30f8602
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)
@@ -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)
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require 'bwa/discovery'
4
- require 'bwa/proxy'
4
+ require "bwa/discovery"
5
+ require "bwa/proxy"
5
6
 
6
7
  Thread.new do
7
8
  BWA::Discovery.advertise
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require 'bwa/discovery'
4
- require 'bwa/server'
4
+ require "bwa/discovery"
5
+ require "bwa/server"
5
6
 
6
7
  Thread.new do
7
8
  BWA::Discovery.advertise
@@ -1 +1,3 @@
1
- require 'bwa/client'
1
+ # frozen_string_literal: true
2
+
3
+ require "bwa/client"