balboa_worldwide_app 1.3.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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"