balboa_worldwide_app 1.3.0 → 2.0.3

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: 3fca5333c7db064572f3179a76f34ae4be4915fe54e6d00d7e1ba3ac018b2faa
4
+ data.tar.gz: 4e8bb6e201feb560bdff1424802d38ce38993f91bb9adb741f629590a7e5fa87
5
5
  SHA512:
6
- metadata.gz: 3ff3043cbbdefd778ee2fbea74ce46ad8c73860f8e541c9035911219b6d6048203f817a2f129bac16678334d4f02cddea806814357d13710c4c76fc3e1987b5e
7
- data.tar.gz: bb1d5468380c06c7bc38c34cf984cd9c4550fb3a3f1314f1b85617df0cff003ece26ce4603746f59f0e00950ae8244aaf0f477149f3b693ab67b8f6ff30f8602
6
+ metadata.gz: 402069abd87db291867298dc70fa9180944972965d3456f4ea99bc44c55a56472fc2592f51edcd3a67c55e9e22b9bfdb184b7f9de6105834128d40c457ea4647
7
+ data.tar.gz: 74e2a3ac63a6e90ceb82dcc97139a30572e62c980420c66b7dac3c156668ddcec999be06b8414958a67fde084af4c6ea9d6312f261d3c36a4569378d7c779f49
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,399 @@
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
+ allow_toggle_or_speed = lambda do |value|
141
+ next value if value == "toggle"
142
+
143
+ value.to_i if value.match?(/^\d+$/)
144
+ end
145
+
146
+ @homie.node("spa", "Hot Tub", @bwa.model) do |spa|
147
+ spa.property("hold",
148
+ "Hold",
149
+ :boolean,
150
+ @bwa.hold,
151
+ hass: { switch: { icon: "mdi:pause-octagon" } },
152
+ non_standard_value_check: allow_toggle) do |value|
153
+ next @bwa.toggle_hold if value == "toggle"
154
+
155
+ @bwa.hold = value
156
+ end
157
+ spa.property("priming", "Priming", :boolean, @bwa.priming, hass: { binary_sensor: { icon: "mdi:fast-forward" } })
158
+ spa.property("heating-mode",
159
+ "Heating Mode",
160
+ :enum,
161
+ @bwa.heating_mode,
162
+ format: BWA::Client::HEATING_MODES,
163
+ hass: { select: { icon: "mdi:cog-play" } },
164
+ non_standard_value_check: allow_toggle) do |value|
165
+ next @bwa.toggle_heating_mode if value == "toggle"
166
+
167
+ @bwa.heating_mode = value.to_sym
168
+ end
169
+ spa.property("temperature-scale",
170
+ "Temperature Scale",
171
+ :enum,
172
+ @bwa.temperature_scale,
173
+ format: %w[fahrenheit celsius],
174
+ hass: :select) do |value|
175
+ @bwa.temperature_scale = value.to_sym
176
+ end
177
+ spa.property("twenty-four-hour-time",
178
+ "24 Hour Time",
179
+ :boolean,
180
+ @bwa.twenty_four_hour_time?,
181
+ hass: { switch: { icon: "mdi:timer-cog" } }) do |value|
182
+ now = Time.now
183
+ @bwa.set_time(now.hour, now.min, twenty_four_hour_time: value)
184
+ end
185
+ spa.property("heating",
186
+ "Heating",
187
+ :boolean,
188
+ @bwa.heating?,
189
+ hass: {
190
+ binary_sensor: { device_class: :running, icon: "mdi:hot-tub" }
191
+ })
192
+ spa.property("temperature-range",
193
+ "Temperature Range",
194
+ :enum,
195
+ @bwa.temperature_range,
196
+ format: %i[high low],
197
+ hass: { select: { icon: "mdi:thermometer-lines" } },
198
+ non_standard_value_check: allow_toggle) do |value|
199
+ next @bwa.toggle_temperature_range if value == "toggle"
200
+
201
+ @bwa.temperature_range = value.to_sym
202
+ end
203
+ spa.property("current-temperature", "Current Water Temperature", :float, @bwa.current_temperature)
204
+ spa.property("target-temperature", "Target Water Temperature", :float, @bwa.target_temperature) do |value|
205
+ @bwa.target_temperature = value
206
+ end
207
+ update_temperature_scale
208
+
209
+ unless @bwa.configuration.blower.zero?
210
+ if @bwa.configuration.blower == 1
211
+ args = [:boolean, !@bwa.blower.zero?]
212
+ kwargs = { hass: { switch: { icon: "mdi:chart-bubble" } } }
213
+ else
214
+ args = [:integer, @bwa.blower]
215
+ kwargs = {
216
+ format: 0..@bwa.configuration.blower,
217
+ hass: { number: { icon: "mdi:chart-bubble" } }
218
+ }
219
+ end
220
+
221
+ spa.property("blower",
222
+ "Blower",
223
+ *args,
224
+ non_standard_value_check: allow_toggle_or_speed,
225
+ **kwargs) do |value|
226
+ next @bwa.toggle_blower if value == "toggle"
227
+
228
+ @bwa.blower = value
229
+ end
230
+ end
231
+
232
+ if @bwa.configuration.mister
233
+ spa.property("mister",
234
+ "Mister",
235
+ :boolean,
236
+ @bwa.mister,
237
+ hass: { switch: { icon: "mdi:sprinkler-fire" } },
238
+ non_standard_value_check: allow_toggle) do |value|
239
+ next @bwa.toggle_mister if value == "toggle"
240
+
241
+ @bwa.mister = value
242
+ end
243
+ end
244
+
245
+ if @bwa.configuration.circulation_pump
246
+ spa.property("circulation-pump",
247
+ "Circulation Pump Running",
248
+ :boolean,
249
+ @bwa.circulation_pump,
250
+ hass: { binary_sensor: { device_class: :running, icon: "mdi:sync" } })
251
+ end
252
+
253
+ single_pump = @bwa.configuration.pumps.count { |speeds| !speeds.zero? } == 1
254
+ @bwa.configuration.pumps.each_with_index do |speeds, i|
255
+ next if speeds.zero?
256
+
257
+ if speeds == 1
258
+ args = [:boolean, !@bwa.pumps[i].zero?]
259
+ kwargs = { hass: { switch: { icon: "mdi:chart-bubble" } } }
260
+ else
261
+ args = [:integer, @bwa.pumps[i]]
262
+ kwargs = { format: 0..speeds,
263
+ hass: { number: { icon: "mdi:chart-bubble" } } }
264
+ end
265
+ name = single_pump ? "Pump" : "Pump #{i + 1}"
266
+ spa.property("pump#{i + 1}",
267
+ name,
268
+ *args,
269
+ non_standard_value_check: allow_toggle_or_speed,
270
+ **kwargs) do |value|
271
+ next @bwa.toggle_pump(i) if value == "toggle"
272
+
273
+ @bwa.set_pump(i, value)
274
+ end
275
+ end
276
+
277
+ single_light = @bwa.configuration.lights.count(&:itself)
278
+ @bwa.configuration.lights.each_with_index do |exists, i|
279
+ next unless exists
280
+
281
+ name = single_light ? "Lights" : "Lights #{i + 1}"
282
+ spa.property("light#{i + 1}",
283
+ name,
284
+ :boolean,
285
+ @bwa.lights[i],
286
+ hass: { light: { icon: "mdi:car-parking-lights" } },
287
+ non_standard_value_check: allow_toggle) do |value|
288
+ next @bwa.toggle_light(i) if value == "toggle"
289
+
290
+ @bwa.set_light(i, value)
291
+ end
292
+ end
293
+
294
+ @bwa.configuration.aux.each_with_index do |exists, i|
295
+ next unless exists
296
+
297
+ spa.property("aux#{i + 1}",
298
+ "Auxiliary #{i + 1}",
299
+ :boolean,
300
+ @bwa.aux[i],
301
+ hass: :switch,
302
+ non_standard_value_check: allow_toggle) do |value|
303
+ next @bwa.toggle_aux(i) if value == "toggle"
304
+
305
+ @bwa.set_aux(i, value)
306
+ end
307
+ end
308
+ end
309
+
310
+ 2.times do |i|
311
+ @homie.node("filter-cycle#{i + 1}", "Filter Cycle #{i + 1}", "Filter Cycle") do |cycle|
312
+ cycle.property("running",
313
+ "Running",
314
+ :boolean,
315
+ @bwa.status.filter_cycles[i],
316
+ hass: { binary_sensor: { icon: "mdi:air-filter" } })
317
+ cycle.property("start-hour",
318
+ "Start Hour",
319
+ :integer,
320
+ @bwa.filter_cycles.public_send(:"cycle#{i + 1}_start_hour"),
321
+ format: 0...24,
322
+ unit: "hours",
323
+ hass: { number: { icon: "mdi:clock" } }) do |value|
324
+ update_filter_cycles(:"cycle#{i + 1}_start_hour", value)
325
+ end
326
+ cycle.property("start-minute",
327
+ "Start Minute",
328
+ :integer,
329
+ @bwa.filter_cycles.public_send(:"cycle#{i + 1}_start_minute"),
330
+ format: 0...60,
331
+ unit: "minutes",
332
+ hass: { number: { icon: "mdi:clock" } }) do |value|
333
+ update_filter_cycles(:"cycle#{i + 1}_start_minute", value)
334
+ end
335
+ cycle.property("duration",
336
+ "Duration",
337
+ :integer,
338
+ @bwa.filter_cycles.public_send(:"cycle#{i + 1}_duration"),
339
+ format: 0...1440,
340
+ unit: "minutes",
341
+ hass: { number: { icon: "mdi:clock" } }) do |value|
342
+ update_filter_cycles(:"cycle#{i + 1}_duration", value)
343
+ end
344
+
345
+ next unless i == 1
346
+
347
+ cycle.property("enabled",
348
+ "Enabled",
349
+ :boolean,
350
+ hass: { switch: { icon: "mdi:filter-check" } }) do |value|
351
+ update_filter_cycles(:cycle2_enabled, value)
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ def update_filter_cycles(prop, value)
358
+ new_config = @bwa.filter_cycles.dup
359
+ new_config.public_send("#{prop}=", value)
360
+ @bwa.update_filter_cycles(new_config)
361
+ end
362
+
363
+ def update_temperature_scale
364
+ @homie["spa"]["current-temperature"].unit =
365
+ @homie["spa"]["target-temperature"].unit =
366
+ "°#{@bwa.temperature_scale.to_s[0].upcase}"
367
+ if @bwa.temperature_scale == :celsius
368
+ @homie["spa"]["current-temperature"].format = 0..42
369
+ @homie["spa"]["target-temperature"].format = 10..40
370
+ else
371
+ @homie["spa"]["current-temperature"].format = 32..108
372
+ @homie["spa"]["target-temperature"].format = 50..106
373
+ end
374
+
375
+ @homie["spa"]["current-temperature"].hass_sensor(device_class: :temperature)
376
+ @homie["spa"]["target-temperature"].hass_number(icon: "mdi:thermometer")
377
+ end
378
+ end
379
+
380
+ mqtt_uri = ARGV.shift
381
+
382
+ if ARGV.empty?
383
+ spas = BWA::Discovery.discover
384
+ if spas.empty?
385
+ BWA.logger.fatal "Could not find spa!"
386
+ warn "Could not find spa!"
387
+ exit 1
388
+ end
389
+ spa_ip = "tcp://#{spas.first.first}/"
390
+ else
391
+ spa_ip = ARGV[0]
392
+ end
393
+
394
+ spa = BWA::Client.new(spa_ip)
395
+
396
+ spa.request_configuration
397
+ spa.request_filter_configuration
398
+
399
+ 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"