balboa_worldwide_app 1.3.0 → 2.0.3

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: 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"