balboa_worldwide_app 1.2.5 → 1.3.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: c54bf4de74bdb4e53dce087127534357ec05b1b0b69921f6d7c6956a566ca844
4
- data.tar.gz: 17a7574e1134c7976505777567fd90209ebb5dc85fd358f132cb4088329a1e07
3
+ metadata.gz: f1f61d30556c0536efad6d27c229cefc0bccd1293a4dc491230a3e68cfddca13
4
+ data.tar.gz: 3014c3c575a53cceaa2f9c2903b1977480bffa5cc0db5e44f08bce8fc8671e17
5
5
  SHA512:
6
- metadata.gz: 0d9feac37083c472fb2609a337cd55d7e9415230f0c3192a76b161278fadf6e51659f54559b3596957c03a486359c3b2d0e5bc78f130685dba07865eac31144a
7
- data.tar.gz: 7e10ebe03a3ac43ecbad06ea3783bbbf517ad2cde7c78f6e0bbed333399a873b6549a0db80990e15fd2b94b74c898190b626f71c49db8c84f3cdf4e25643707a
6
+ metadata.gz: 3ff3043cbbdefd778ee2fbea74ce46ad8c73860f8e541c9035911219b6d6048203f817a2f129bac16678334d4f02cddea806814357d13710c4c76fc3e1987b5e
7
+ data.tar.gz: bb1d5468380c06c7bc38c34cf984cd9c4550fb3a3f1314f1b85617df0cff003ece26ce4603746f59f0e00950ae8244aaf0f477149f3b693ab67b8f6ff30f8602
data/bin/bwa_client CHANGED
@@ -27,7 +27,7 @@ if ARGV.empty?
27
27
  $stderr.puts "Could not find spa!"
28
28
  exit 1
29
29
  end
30
- spa_ip = spas.first.first
30
+ spa_ip = 'tcp://' + spas.first.first + '/'
31
31
  else
32
32
  spa_ip = ARGV[0]
33
33
  end
data/bin/bwa_mqtt_bridge CHANGED
@@ -3,9 +3,12 @@
3
3
  require 'mqtt'
4
4
  require 'sd_notify'
5
5
  require 'set'
6
+ require 'json'
6
7
 
8
+ require 'bwa/logger'
7
9
  require 'bwa/client'
8
10
  require 'bwa/discovery'
11
+ require 'bwa/version'
9
12
 
10
13
  class MQTTBridge
11
14
  def initialize(mqtt_uri, bwa, device_id: "bwa", base_topic: "homie")
@@ -17,9 +20,36 @@ class MQTTBridge
17
20
  @attributes = {}
18
21
  @things = Set.new
19
22
 
23
+ hass_discovery_topic = "homeassistant/"
24
+ @ha_binary_path = hass_discovery_topic + "binary_sensor/"
25
+ @ha_sensor_path = hass_discovery_topic + "sensor/"
26
+ @ha_switch_path = hass_discovery_topic + "switch/"
27
+ @ha_selects_path = hass_discovery_topic + "select/"
28
+ @ha_number_path = hass_discovery_topic + "number/"
29
+
30
+ @hass_device = {
31
+ device: {
32
+ identifiers: [device_id],
33
+ sw_version: BWA::VERSION,
34
+ name: "BWA Link"
35
+ }
36
+ }
37
+
38
+ # bwa availability topics (works for all devices)
39
+ @hass_availability = {
40
+ availability_topic: "#{@base_topic}/$state",
41
+ payload_available: "ready",
42
+ payload_not_available: "lost"
43
+ }
44
+
20
45
  publish_basic_attributes
46
+ publish_filtercycles
47
+
48
+ # Home Assistant MQTT Discovery Section
49
+ publish_hass_discovery
21
50
 
22
51
  # Tell systemd we've started up OK. Ignored if systemd not in use.
52
+ BWA.logger.warn "Balboa MQTT Bridge running (version #{BWA::VERSION})"
23
53
  SdNotify.ready
24
54
 
25
55
  bwa_thread = Thread.new do
@@ -28,7 +58,6 @@ class MQTTBridge
28
58
  message = @bwa.poll
29
59
  next if message.is_a?(BWA::Messages::Ready)
30
60
 
31
- puts message.inspect unless message.is_a?(BWA::Messages::Status)
32
61
  case message
33
62
  when BWA::Messages::ControlConfiguration
34
63
  publish("spa/$type", message.model)
@@ -37,18 +66,29 @@ class MQTTBridge
37
66
  publish_pump(i + 1, speed) if speed != 0
38
67
  end
39
68
  message.lights.each_with_index do |exists, i|
40
- publish_thing("light", i + 1) if exists
69
+ publish_thing(:light, i + 1) if exists
41
70
  end
42
71
  message.aux.each_with_index do |exists, i|
43
- publish_thing("aux", i + 1) if exists
72
+ publish_thing(:aux, i + 1) if exists
44
73
  end
45
74
  publish_mister if message.mister
46
75
  publish_blower(message.blower) if message.blower != 0
47
76
  publish_circpump if message.circ_pump
48
77
  publish("$state", "ready")
78
+ when BWA::Messages::FilterCycles
79
+ publish_attribute("spa/filter1hour",message.filter1_hour)
80
+ publish_attribute("spa/filter1minute",message.filter1_minute)
81
+ publish_attribute("spa/filter1durationhours",message.filter1_duration_hours)
82
+ publish_attribute("spa/filter1durationminutes",message.filter1_duration_minutes)
83
+ publish_attribute("spa/filter2enabled",message.filter2_enabled)
84
+ publish_attribute("spa/filter2hour",message.filter2_hour)
85
+ publish_attribute("spa/filter2minute",message.filter2_minute)
86
+ publish_attribute("spa/filter2durationhours",message.filter2_duration_hours)
87
+ publish_attribute("spa/filter2durationminutes",message.filter2_duration_minutes)
49
88
  when BWA::Messages::Status
50
89
  @bwa.request_control_info unless @bwa.last_control_configuration
51
90
  @bwa.request_control_info2 unless @bwa.last_control_configuration2
91
+ @bwa.request_filter_configuration unless @bwa.last_filter_configuration
52
92
 
53
93
  # make sure time is in sync
54
94
  now = Time.now
@@ -59,8 +99,10 @@ class MQTTBridge
59
99
 
60
100
  # allow a skew of 1 minute, since the seconds will always be off
61
101
  if diff > 1
102
+ BWA.logger.info "Spa time #{"%02d:%02d" % [message.hour, message.minute]}, actually #{"%02d:%02d" % [now.hour, now.min]}; correcting difference of #{diff} min"
62
103
  @bwa.set_time(now.hour, now.min, message.twenty_four_hour_time)
63
104
  end
105
+ publish_attribute("spa/hold", message.hold)
64
106
  publish_attribute("spa/priming", message.priming)
65
107
  publish_attribute("spa/heatingmode", message.heating_mode)
66
108
  publish_attribute("spa/temperaturescale", message.temperature_scale)
@@ -72,11 +114,13 @@ class MQTTBridge
72
114
  publish_attribute("spa/settemperature", message.set_temperature)
73
115
  publish_attribute("spa/settemperature/$unit", "º#{message.temperature_scale.to_s[0].upcase}")
74
116
  if message.temperature_scale == :celsius
75
- publish_attribute("spa/currenttemperature/$format", message.temperature_range == :high ? "26:40" : "10:26")
117
+ publish_attribute("spa/currenttemperature/$format", "0:42")
76
118
  publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "26:40" : "10:26")
119
+ publish_hass_discovery_settemp(:celsius)
77
120
  else
78
- publish_attribute("spa/currenttemperature/$format", message.temperature_range == :high ? "80:104" : "26:40")
79
- publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "80:104" : "26:40")
121
+ publish_attribute("spa/currenttemperature/$format", "32:108")
122
+ publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "80:106" : "50:99")
123
+ publish_hass_discovery_settemp(:fahrenheit)
80
124
  end
81
125
  publish_attribute("spa/filter1", message.filter[0])
82
126
  publish_attribute("spa/filter2", message.filter[1])
@@ -96,14 +140,19 @@ class MQTTBridge
96
140
 
97
141
  # Tell systemd we are still alive and kicking. Ignored if systemd not in use.
98
142
  SdNotify.watchdog
143
+
99
144
  end
100
145
  end
101
146
  end
102
147
  end
103
148
 
104
149
  @mqtt.get do |topic, value|
105
- puts "got #{value.inspect} at #{topic}"
150
+ BWA.logger.warn "from mqtt: #{value.inspect} at #{topic}"
106
151
  case topic[@base_topic.length + 1..-1]
152
+ when "spa/hold/set"
153
+ next @bwa.toggle_hold if value == 'toggle'
154
+ next unless %w{true false}.include?(value)
155
+ @bwa.set_hold(value == 'true')
107
156
  when "spa/heatingmode/set"
108
157
  next @bwa.toggle_heating_mode if value == 'toggle'
109
158
  next unless %w{ready rest}.include?(value)
@@ -135,11 +184,14 @@ class MQTTBridge
135
184
  @bwa.set_blower(value.to_i)
136
185
  when "spa/settemperature/set"
137
186
  @bwa.set_temperature(value.to_f)
187
+ when %r{^spa/(filter[12](hour|minute|durationhours|durationminutes|enabled))/set$}
188
+ @bwa.set_filtercycles($1, value)
138
189
  end
139
190
  end
140
191
  end
141
192
 
142
193
  def publish(topic, value)
194
+ BWA.logger.debug " to mqtt: #{topic}: #{value}"
143
195
  @mqtt.publish("#{@base_topic}/#{topic}", value, true)
144
196
  end
145
197
 
@@ -154,6 +206,216 @@ class MQTTBridge
154
206
  @mqtt.subscribe("#{@base_topic}/#{topic}")
155
207
  end
156
208
 
209
+ def publish_hass_discovery()
210
+ #Priming
211
+ priming_config = {
212
+ name: "Hot Tub Priming",
213
+ state_topic: "#{@base_topic}/spa/priming",
214
+ device_class: "running",
215
+ unique_id: "spa_priming",
216
+ icon: "mdi:fast-forward"
217
+ }
218
+ @mqtt.publish(@ha_binary_path + "priming/config", priming_config
219
+ .merge(@hass_device)
220
+ .merge(@hass_availability)
221
+ .to_json, true)
222
+
223
+ #Circulation Pump
224
+ circ_config = {
225
+ name: "Hot Tub Circulation Pump",
226
+ state_topic: "#{@base_topic}/spa/circpump",
227
+ device_class: "running",
228
+ unique_id: "spa_circpump",
229
+ icon: "mdi:sync"
230
+ }
231
+ @mqtt.publish(@ha_binary_path + "circpump/config", circ_config
232
+ .merge(@hass_device)
233
+ .merge(@hass_availability)
234
+ .to_json, true)
235
+
236
+ # Filter 1 Cycle Running
237
+ filter1_config = {
238
+ name: "Hot Tub Filter 1 Cycle Running",
239
+ state_topic: "#{@base_topic}/spa/filter1",
240
+ device_class: "running",
241
+ unique_id: "spa_filtercycle1",
242
+ icon: "mdi:air-filter"
243
+ }
244
+ @mqtt.publish(@ha_binary_path + "filter1/config", filter1_config
245
+ .merge(@hass_device)
246
+ .merge(@hass_availability)
247
+ .to_json, true)
248
+
249
+ # Filter 2 Cycle Running
250
+ filter2_config = {
251
+ name: "Hot Tub Filter 2 Cycle Running",
252
+ state_topic: "#{@base_topic}/spa/filter2",
253
+ device_class: "running",
254
+ unique_id: "spa_filtercycle2",
255
+ icon: "mdi:air-filter"
256
+ }
257
+ @mqtt.publish(@ha_binary_path + "filter2/config", filter2_config
258
+ .merge(@hass_device)
259
+ .merge(@hass_availability)
260
+ .to_json, true)
261
+
262
+ # Heater Running
263
+ heater_config = {
264
+ name: "Hot Tub Heater",
265
+ state_topic: "#{@base_topic}/spa/heating",
266
+ device_class: "running",
267
+ unique_id: "spa_heating",
268
+ icon: "mdi:hot-tub"
269
+ }
270
+ @mqtt.publish(@ha_binary_path + "heating/config", heater_config
271
+ .merge(@hass_device)
272
+ .merge(@hass_availability)
273
+ .to_json, true)
274
+
275
+ # Heating Mode
276
+ heatingmode_config = {
277
+ name: "Hot Tub Heating Mode",
278
+ state_topic: "#{@base_topic}/spa/heatingmode",
279
+ command_topic: "#{@base_topic}/spa/heatingmode/set",
280
+ unique_id: "spa_heating_mode",
281
+ options: ["ready","rest","ready_in_rest"],
282
+ icon: "mdi:cog-play"
283
+ }
284
+ @mqtt.publish(@ha_selects_path + "heatingmode/config", heatingmode_config
285
+ .merge(@hass_device)
286
+ .merge(@hass_availability)
287
+ .to_json, true)
288
+
289
+ # Temperature Range
290
+ temperaturerange_config = {
291
+ name: "Hot Tub Temperature Range",
292
+ state_topic: "#{@base_topic}/spa/temperaturerange",
293
+ command_topic: "#{@base_topic}/spa/temperaturerange/set",
294
+ unique_id: "spa_temperaturerange_range",
295
+ options: ["high","low"],
296
+ icon: "mdi:thermometer-lines"
297
+ }
298
+ @mqtt.publish(@ha_selects_path + "temperaturerange/config", temperaturerange_config
299
+ .merge(@hass_device)
300
+ .merge(@hass_availability)
301
+ .to_json, true)
302
+
303
+ # Temperature Scale
304
+ temperaturescale_config = {
305
+ name: "Hot Tub Temperature Scale",
306
+ state_topic: "#{@base_topic}/spa/temperaturescale",
307
+ unique_id: "spa_temperaturescale"
308
+ }
309
+ @mqtt.publish(@ha_sensor_path + "temperaturescale/config", temperaturescale_config
310
+ .merge(@hass_device)
311
+ .merge(@hass_availability)
312
+ .to_json, true)
313
+
314
+ # Current Temperature
315
+ currenttemperature_config = {
316
+ name: "Hot Tub Current Temperature",
317
+ state_topic: "#{@base_topic}/spa/currenttemperature",
318
+ unique_id: "spa_currenttemperature",
319
+ device_class: "temperature"
320
+ }
321
+ @mqtt.publish(@ha_sensor_path + "currenttemperature/config", currenttemperature_config
322
+ .merge(@hass_device)
323
+ .merge(@hass_availability)
324
+ .to_json, true)
325
+
326
+ # Set Temperature - Assuming fahrenheit first
327
+ settemperature_config = {
328
+ name: "Hot Tub Set Temperature",
329
+ unique_id: "spa_settemperature",
330
+ state_topic: "#{@base_topic}/spa/settemperature",
331
+ command_topic: "#{@base_topic}/spa/settemperature/set",
332
+ min: 50,
333
+ max: 106,
334
+ unit_of_measurement: "°F",
335
+ icon: "mdi:thermometer"
336
+ }
337
+ @mqtt.publish(@ha_number_path + "settemperature/config", settemperature_config
338
+ .merge(@hass_device)
339
+ .merge(@hass_availability)
340
+ .to_json, true)
341
+ end
342
+
343
+ def publish_hass_discovery_settemp(scale)
344
+ return if scale == @last_scale
345
+ @last_scale = scale
346
+
347
+ # Update temp ranges when scale changes
348
+ settemperature_config = {
349
+ name: "Hot Tub Set Temperature",
350
+ unique_id: "spa_settemperature",
351
+ state_topic: "#{@base_topic}/spa/settemperature",
352
+ command_topic: "#{@base_topic}/spa/settemperature/set",
353
+ min: 50,
354
+ max: 106,
355
+ unit_of_measurement: "°F",
356
+ icon: "mdi:thermometer"
357
+ }
358
+
359
+ if scale == :celsius
360
+ settemperature_config[:min] = 10
361
+ settemperature_config[:max] = 40
362
+ settemperature_config[:unit_of_measurement] = "°C"
363
+ else
364
+ settemperature_config[:min] = 50
365
+ settemperature_config[:max] = 106
366
+ settemperature_config[:unit_of_measurement] = "°F"
367
+ end
368
+
369
+ @mqtt.publish(@ha_number_path + "settemperature/config", settemperature_config
370
+ .merge(@hass_device)
371
+ .merge(@hass_availability)
372
+ .to_json, true)
373
+ end
374
+
375
+ def publish_hass_discovery_pumps(i, speeds)
376
+ pump_config = {
377
+ name: "Hot Tub Pump #{i}",
378
+ unique_id: "spa_pump#{i}",
379
+ state_topic: "#{@base_topic}/spa/pump#{i}",
380
+ command_topic: "#{@base_topic}/spa/pump#{i}/set",
381
+ payload_on: "toggle",
382
+ payload_off: "toggle",
383
+ state_on: "2",
384
+ state_off: "0",
385
+ icon: "mdi:chart-bubble"
386
+ }
387
+ @mqtt.publish(@ha_switch_path + "pump#{i}/config", pump_config
388
+ .merge(@hass_device)
389
+ .merge(@hass_availability)
390
+ .to_json, true)
391
+ end
392
+
393
+ def publish_hass_discovery_things(type, i)
394
+ #Things - Lights and such
395
+ thing_config = {
396
+ name: "Hot Tub #{type} #{i}",
397
+ unique_id: "spa_#{type}#{i}",
398
+ state_topic: "#{@base_topic}/spa/#{type}#{i}",
399
+ command_topic: "#{@base_topic}/spa/#{type}#{i}/set",
400
+ payload_on: "true",
401
+ payload_off: "false",
402
+ state_on: "true",
403
+ state_off: "false"
404
+ }
405
+ case type
406
+ when :light
407
+ thing_config[:icon] = "mdi:car-parking-lights"
408
+ when :mister
409
+ thing_config[:icon] = "mdi:sprinkler-fire"
410
+ when :blower
411
+ thing_config[:icon] = "mdi:chart-bubble"
412
+ end
413
+ @mqtt.publish(@ha_switch_path+ "#{type}#{i}/config", thing_config
414
+ .merge(@hass_device)
415
+ .merge(@hass_availability)
416
+ .to_json, true)
417
+ end
418
+
157
419
  def publish_basic_attributes
158
420
  publish("$homie", "4.0.0")
159
421
  publish("$name", "BWA Spa")
@@ -164,6 +426,11 @@ class MQTTBridge
164
426
  publish("spa/$type", "spa")
165
427
  publish_nodes
166
428
 
429
+ publish("spa/hold/$name", "In Hold mode")
430
+ publish("spa/hold/$datatype", "boolean")
431
+ publish("spa/hold/$settable", "true")
432
+ subscribe("spa/hold/set")
433
+
167
434
  publish("spa/priming/$name", "Is the pump priming")
168
435
  publish("spa/priming/$datatype", "boolean")
169
436
 
@@ -216,6 +483,7 @@ class MQTTBridge
216
483
  subscribe("spa/pump#{i}/set")
217
484
 
218
485
  @things << "pump#{i}"
486
+ publish_hass_discovery_pumps(i, speeds)
219
487
  publish_nodes
220
488
  end
221
489
 
@@ -226,6 +494,7 @@ class MQTTBridge
226
494
  subscribe("spa/#{type}#{i}/set")
227
495
 
228
496
  @things << "#{type}#{i}"
497
+ publish_hass_discovery_things(type, i)
229
498
  publish_nodes
230
499
  end
231
500
 
@@ -258,8 +527,68 @@ class MQTTBridge
258
527
  publish_nodes
259
528
  end
260
529
 
530
+ def publish_filtercycles
531
+ publish("spa/filter1hour/$name", "Filter Cycle 1 Start Hour")
532
+ publish("spa/filter1hour/$datatype", "integer")
533
+ publish("spa/filter1hour/$format", "0:24")
534
+ publish("spa/filter1hour/$settable", "true")
535
+
536
+ publish("spa/filter1minute/$name", "Filter Cycle 1 Start Minutes")
537
+ publish("spa/filter1minute/$datatype", "integer")
538
+ publish("spa/filter1minute/$format", "0:59")
539
+ publish("spa/filter1minute/$settable", "true")
540
+
541
+ publish("spa/filter1durationhours/$name", "Filter Cycle 1 Duration Hours")
542
+ publish("spa/filter1durationhours/$datatype", "integer")
543
+ publish("spa/filter1durationhours/$format", "0:24")
544
+ publish("spa/filter1durationhours/$settable", "true")
545
+
546
+ publish("spa/filter1durationminutes/$name", "Filter Cycle 1 Duration Minutes")
547
+ publish("spa/filter1durationminutes/$datatype", "integer")
548
+ publish("spa/filter1durationminutes/$format", "0:59")
549
+ publish("spa/filter1durationminutes/$settable", "true")
550
+
551
+ publish("spa/filter2enabled/$name", "Filter Cycle 2 Enabled")
552
+ publish("spa/filter2enabled/$datatype", "boolean")
553
+ publish("spa/filter2enabled/$settable", "true")
554
+
555
+ publish("spa/filter2hour/$name", "Filter Cycle 2 Start Hour")
556
+ publish("spa/filter2hour/$datatype", "integer")
557
+ publish("spa/filter2hour/$format", "0:24")
558
+ publish("spa/filter2hour/$settable", "true")
559
+
560
+ publish("spa/filter2minute/$name", "Filter Cycle 2 Start Minutes")
561
+ publish("spa/filter2minute/$datatype", "integer")
562
+ publish("spa/filter2minute/$format", "0:59")
563
+ publish("spa/filter2minute/$settable", "true")
564
+
565
+ publish("spa/filter2durationhours/$name", "Filter Cycle 2 Duration Hours")
566
+ publish("spa/filter2durationhours/$datatype", "integer")
567
+ publish("spa/filter2durationhours/$format", "0:24")
568
+ publish("spa/filter2durationhours/$settable", "true")
569
+
570
+ publish("spa/filter2durationminutes/$name", "Filter Cycle 2 Duration Minutes")
571
+ publish("spa/filter2durationminutes/$datatype", "integer")
572
+ publish("spa/filter2durationminutes/$format", "0:59")
573
+ publish("spa/filter2durationminutes/$settable", "true")
574
+
575
+ subscribe("spa/filter1hour/set")
576
+ subscribe("spa/filter1minute/set")
577
+ subscribe("spa/filter1durationhours/set")
578
+ subscribe("spa/filter1durationminutes/set")
579
+ subscribe("spa/filter2enabled/set")
580
+ subscribe("spa/filter2hour/set")
581
+ subscribe("spa/filter2minute/set")
582
+ subscribe("spa/filter2durationhours/set")
583
+ subscribe("spa/filter2durationminutes/set")
584
+
585
+ @things.merge(["filter1hour,filter1minute,filter1durationhours,filter1durationminutes,filter2enabled,filter2hour,filter2minute,filter2durationhours,filter2durationminutes"])
586
+
587
+ publish_nodes
588
+ end
589
+
261
590
  def publish_nodes
262
- publish("spa/$properties", (["priming,heatingmode,temperaturescale,24htime,heating,temperaturerange,currenttemperature,settemperature,filter1,filter2"] + @things.to_a).join(','))
591
+ publish("spa/$properties", (["hold,priming,heatingmode,temperaturescale,24htime,heating,temperaturerange,currenttemperature,settemperature,filter1,filter2"] + @things.to_a).join(','))
263
592
  end
264
593
  end
265
594
 
@@ -268,6 +597,7 @@ mqtt_uri = ARGV.shift
268
597
  if ARGV.empty?
269
598
  spas = BWA::Discovery.discover
270
599
  if spas.empty?
600
+ BWA.logger.fatal "Could not find spa!"
271
601
  $stderr.puts "Could not find spa!"
272
602
  exit 1
273
603
  end
@@ -279,5 +609,6 @@ end
279
609
  spa = BWA::Client.new(spa_ip)
280
610
 
281
611
  spa.request_configuration
612
+ spa.request_filter_configuration
282
613
 
283
614
  MQTTBridge.new(mqtt_uri, spa)
data/lib/bwa/client.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'uri'
2
2
 
3
+ require 'bwa/logger'
3
4
  require 'bwa/message'
4
5
 
5
6
  module BWA
@@ -10,7 +11,7 @@ module BWA
10
11
  uri = URI.parse(uri)
11
12
  if uri.scheme == 'tcp'
12
13
  require 'socket'
13
- @io = TCPSocket.new(uri.host, uri.port || 4217)
14
+ @io = TCPSocket.new(uri.host, uri.port || 4257)
14
15
  elsif uri.scheme == 'telnet' || uri.scheme == 'rfc2217'
15
16
  require 'net/telnet/rfc2217'
16
17
  @io = Net::Telnet::RFC2217.new("Host" => uri.host, "Port" => uri.port || 23, "baud" => 115200)
@@ -20,6 +21,7 @@ module BWA
20
21
  @io = CCutrer::SerialPort.new(uri.path, baud: 115200)
21
22
  @queue = []
22
23
  end
24
+ @src = 0x0a
23
25
  @buffer = ""
24
26
  end
25
27
 
@@ -43,7 +45,7 @@ module BWA
43
45
  end
44
46
 
45
47
  if message.is_a?(Messages::Ready) && (msg = @queue&.shift)
46
- puts "wrote #{msg.unpack('H*').first}"
48
+ BWA.logger.debug "wrote: #{BWA.raw2str(msg)}" unless BWA.verbosity < 1 && msg[3..4] == Messages::ControlConfigurationRequest::MESSAGE_TYPE
47
49
  @io.write(msg)
48
50
  end
49
51
  @last_status = message.dup if message.is_a?(Messages::Status)
@@ -62,35 +64,35 @@ module BWA
62
64
  end
63
65
 
64
66
  def send_message(message)
65
- length = message.length + 2
66
- full_message = "#{length.chr}#{message}".force_encoding(Encoding::ASCII_8BIT)
67
- checksum = CRC.checksum(full_message)
68
- full_message = "\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT)
67
+ message.src = @src
68
+ BWA.logger.info " to spa: #{message.inspect}" unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
69
+ full_message = message.serialize
69
70
  if @queue
70
71
  @queue.push(full_message)
71
72
  else
73
+ BWA.logger.debug "wrote: #{BWA.raw2str(full_message)}" unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
72
74
  @io.write(full_message)
73
75
  end
74
76
  end
75
77
 
76
78
  def request_configuration
77
- send_message("\x0a\xbf\x04")
79
+ send_message(Messages::ConfigurationRequest.new)
78
80
  end
79
81
 
80
82
  def request_control_info2
81
- send_message("\x0a\xbf\x22\x00\x00\x01")
83
+ send_message(Messages::ControlConfigurationRequest.new(2))
82
84
  end
83
85
 
84
86
  def request_control_info
85
- send_message("\x0a\xbf\x22\x02\x00\x00")
87
+ send_message(Messages::ControlConfigurationRequest.new(1))
86
88
  end
87
89
 
88
90
  def request_filter_configuration
89
- send_message("\x0a\xbf\x22\x01\x00\x00")
91
+ send_message(Messages::ControlConfigurationRequest.new(3))
90
92
  end
91
93
 
92
94
  def toggle_item(item)
93
- send_message("\x0a\xbf\x11#{item.chr}\x00")
95
+ send_message(Messages::ToggleItem.new(item))
94
96
  end
95
97
 
96
98
  def toggle_pump(i)
@@ -102,11 +104,15 @@ module BWA
102
104
  end
103
105
 
104
106
  def toggle_mister
105
- toggle_item(0x0e)
107
+ toggle_item(:mister)
106
108
  end
107
109
 
108
110
  def toggle_blower
109
- toggle_item(0x0c)
111
+ toggle_item(:blower)
112
+ end
113
+
114
+ def toggle_hold
115
+ toggle_item(:hold)
110
116
  end
111
117
 
112
118
  def set_pump(i, desired)
@@ -143,22 +149,57 @@ module BWA
143
149
  end
144
150
  end
145
151
 
146
- # high range is 80-104 for F, 26-40 for C (by 0.5)
147
- # low range is 50-80 for F, 10-26 for C (by 0.5)
152
+ def set_hold(desired)
153
+ return unless last_status
154
+ return if last_status.hold == desired
155
+ toggle_hold
156
+ end
157
+
158
+ # high range is 80-106 for F, 26-40 for C (by 0.5)
159
+ # low range is 50-99 for F, 10-26 for C (by 0.5)
148
160
  def set_temperature(desired)
161
+ return unless last_status
162
+ return if last_status.set_temperature == desired
163
+
149
164
  desired *= 2 if last_status && last_status.temperature_scale == :celsius || desired < 50
150
- send_message("\x0a\xbf\x20#{desired.round.chr}")
165
+ send_message(Messages::SetTemperature.new(desired.round))
151
166
  end
152
167
 
153
168
  def set_time(hour, minute, twenty_four_hour_time = false)
154
- hour |= 0x80 if twenty_four_hour_time
155
- send_message("\x0a\xbf\x21".force_encoding(Encoding::ASCII_8BIT) + hour.chr + minute.chr)
169
+ send_message(Messages::SetTime.new(hour, minute, twenty_four_hour_time))
156
170
  end
157
171
 
158
172
  def set_temperature_scale(scale)
159
173
  raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I{fahrenheit :celsius}.include?(scale)
160
- arg = scale == :fahrenheit ? 0 : 1
161
- send_message("\x0a\xbf\x27\x01".force_encoding(Encoding::ASCII_8BIT) + arg.chr)
174
+ send_message(Messages::SetTemperatureScale.new(scale))
175
+ end
176
+
177
+ def set_filtercycles(changedItem, changedValue)
178
+ #changedItem - String name of item that was changed
179
+ #changedValue - String value of the item that changed
180
+ if @last_filter_configuration
181
+ messagedata = if changedItem == "filter1hour" then changedValue.to_i.chr else @last_filter_configuration.filter1_hour.chr end
182
+ messagedata += if changedItem == "filter1minute" then changedValue.to_i.chr else @last_filter_configuration.filter1_minute.chr end
183
+ messagedata += if changedItem == "filter1durationhours" then changedValue.to_i.chr else @last_filter_configuration.filter1_duration_hours.chr end
184
+ messagedata += if changedItem == "filter1durationminutes" then changedValue.to_i.chr else @last_filter_configuration.filter1_duration_minutes.chr end
185
+
186
+ #The filter2 start hour is merged with the filter2 enable (who thought that was a good idea?) The high order bit of the byte is a flag
187
+ #to indicate this so we have to do a bit of different processing to do that
188
+ #Get the filter 2 start hour
189
+ starthour = if changedItem == "filter2hour" then changedValue.to_i else @last_filter_configuration.filter2_hour end
190
+ #Check to see if we want filter 2 enabled (either because it changed or from the current configuration)
191
+ #If it is something that changed, we have to convert to boolean, if it is from the current config it already is a boolean
192
+ starthour |= 0x80 if (if changedItem == "filter2enabled" then (changedValue == "true" ? true : false) else @last_filter_configuration.filter2_enabled end)
193
+
194
+ messagedata += starthour.chr
195
+
196
+ messagedata += if changedItem == "filter2minute" then changedValue.to_i.chr else @last_filter_configuration.filter2_minute.chr end
197
+ messagedata += if changedItem == "filter2durationhours" then changedValue.to_i.chr else @last_filter_configuration.filter2_duration_hours.chr end
198
+ messagedata += if changedItem == "filter2durationminutes" then changedValue.to_i.chr else @last_filter_configuration.filter2_duration_minutes.chr end
199
+
200
+ send_message("\x0a\xbf\x23".force_encoding(Encoding::ASCII_8BIT) + messagedata)
201
+ end
202
+ request_filter_configuration
162
203
  end
163
204
 
164
205
  def toggle_temperature_range
@@ -172,7 +213,7 @@ module BWA
172
213
  end
173
214
 
174
215
  def toggle_heating_mode
175
- toggle_item(0x51)
216
+ toggle_item(:heating_mode)
176
217
  end
177
218
 
178
219
  HEATING_MODES = %I{ready rest ready_in_rest}.freeze
data/lib/bwa/discovery.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'bwa/logger'
2
3
 
3
4
  module BWA
4
5
  class Discovery
@@ -34,7 +35,7 @@ module BWA
34
35
  data, addr = socket.recvfrom(32)
35
36
  next unless data == 'Discovery: Who is out there?'
36
37
  ip = addr.last
37
- puts "Advertising to #{ip}"
38
+ BWA.logger.info "Advertising to #{ip}"
38
39
  socket.sendmsg(msg, 0, Socket.sockaddr_in(addr[1], ip))
39
40
  end
40
41
  end
data/lib/bwa/logger.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'logger'
2
+
3
+ module BWA
4
+ # This module logs to stdout by default, or you can provide a logger as BWA.logger.
5
+ # If using default logger, set LOG_LEVEL in the environment to control logging.
6
+ #
7
+ # Log levels are:
8
+ #
9
+ # FATAL - fatal errors
10
+ # ERROR - handled errors
11
+ # WARN - problems while parsing known messages
12
+ # INFO - unrecognized messages
13
+ # DEBUG - all messages
14
+ #
15
+ # Certain very frequent messages are suppressed by default even in DEBUG mode.
16
+ # Set LOG_VERBOSITY to one of the following levels to see these:
17
+ #
18
+ # 0 - default
19
+ # 1 - show status messages
20
+ # 2 - show ready and nothing-to-send messages
21
+ #
22
+ class << self
23
+ attr_writer :logger, :verbosity
24
+
25
+ def logger
26
+ @logger ||= Logger.new(STDOUT).tap do |log|
27
+ STDOUT.sync = true
28
+ log.level = ENV.fetch("LOG_LEVEL","WARN")
29
+ log.formatter = proc do |severity, datetime, progname, msg|
30
+ "#{severity[0..0]}, #{msg2logstr(msg)}\n"
31
+ end
32
+ end
33
+ end
34
+
35
+ def verbosity
36
+ @verbosity ||= ENV.fetch("LOG_VERBOSITY", "0").to_i
37
+ @verbosity
38
+ end
39
+
40
+ def msg2logstr(msg)
41
+ case msg
42
+ when ::String
43
+ msg
44
+ when ::Exception
45
+ "#{ msg.message } (#{ msg.class })\n#{ msg.backtrace.join("\n") if msg.backtrace }"
46
+ else
47
+ msg.inspect
48
+ end
49
+ end
50
+
51
+ def raw2str(data)
52
+ data.unpack("H*").first.gsub!(/(..)/, "\\1 ").chop!
53
+ end
54
+ end
55
+ end
data/lib/bwa/message.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'bwa/logger'
1
2
  require 'bwa/crc'
2
3
 
3
4
  module BWA
@@ -11,6 +12,8 @@ module BWA
11
12
  end
12
13
 
13
14
  class Message
15
+ attr_accessor :src
16
+
14
17
  class Unrecognized < Message
15
18
  end
16
19
 
@@ -20,11 +23,39 @@ module BWA
20
23
  @messages << klass
21
24
  end
22
25
 
26
+ # Ignore (parse and throw away) messages of these types.
27
+ IGNORED_MESSAGES = [
28
+ "\xbf\x00".force_encoding(Encoding::ASCII_8BIT), # request for new clients
29
+ "\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
30
+ "\xbf\x07".force_encoding(Encoding::ASCII_8BIT), # nothing to send
31
+ ]
32
+
33
+ # Don't log messages of these types, even in DEBUG mode.
34
+ # They are very frequent and would swamp the logs.
35
+ def common_messages
36
+ @COMMON_MESSAGES ||= begin
37
+ msgs = []
38
+ msgs += [
39
+ Messages::Status::MESSAGE_TYPE,
40
+ "\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
41
+ ] unless BWA.verbosity >= 1
42
+ msgs += [
43
+ "\xbf\x00".force_encoding(Encoding::ASCII_8BIT),
44
+ "\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
45
+ Messages::Ready::MESSAGE_TYPE,
46
+ "\xbf\x07".force_encoding(Encoding::ASCII_8BIT),
47
+ ] unless BWA.verbosity >= 2
48
+ msgs
49
+ end
50
+ @COMMON_MESSAGES
51
+ end
52
+
23
53
  def parse(data)
24
54
  offset = -1
25
55
  message_type = length = message_class = nil
26
56
  loop do
27
57
  offset += 1
58
+ # Not enough data for a full message; return and hope for more
28
59
  return nil if data.length - offset < 5
29
60
 
30
61
  # Keep scanning until message start char
@@ -52,18 +83,15 @@ module BWA
52
83
  break
53
84
  end
54
85
 
55
- puts "discarding invalid data prior to message #{data[0...offset].unpack('H*').first}" unless offset == 0
56
- #puts "read #{data.slice(offset, length + 2).unpack('H*').first}"
86
+ message_type = data.slice(offset + 3, 2)
87
+ BWA.logger.debug "discarding invalid data prior to message #{BWA.raw2str(data[0...offset])}" unless offset == 0
88
+ BWA.logger.debug " read: #{BWA.raw2str(data.slice(offset, length + 2))}" unless common_messages.include?(message_type)
57
89
 
58
90
  src = data[offset + 2].ord
59
- message_type = data.slice(offset + 3, 2)
60
91
  klass = @messages.find { |k| k::MESSAGE_TYPE == message_type }
61
92
 
62
-
63
- return [nil, offset + length + 2] if [
64
- "\xbf\x00".force_encoding(Encoding::ASCII_8BIT),
65
- "\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
66
- "\xbf\x07".force_encoding(Encoding::ASCII_8BIT)].include?(message_type)
93
+ # Ignore these message types
94
+ return [nil, offset + length + 2] if IGNORED_MESSAGES.include?(message_type)
67
95
 
68
96
  if klass
69
97
  valid_length = if klass::MESSAGE_LENGTH.respond_to?(:include?)
@@ -73,6 +101,7 @@ module BWA
73
101
  end
74
102
  raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}", data) unless valid_length
75
103
  else
104
+ BWA.logger.info "Unrecognized message type #{BWA.raw2str(message_type)}: #{BWA.raw2str(data.slice(offset, length + 2))}"
76
105
  klass = Unrecognized
77
106
  end
78
107
 
@@ -80,6 +109,7 @@ module BWA
80
109
  message.parse(data.slice(offset + 5, length - 5))
81
110
  message.instance_variable_set(:@raw_data, data.slice(offset, length + 2))
82
111
  message.instance_variable_set(:@src, src)
112
+ BWA.logger.debug "from spa: #{message.inspect}" unless common_messages.include?(message_type)
83
113
  [message, offset + length + 2]
84
114
  end
85
115
 
@@ -3,6 +3,10 @@ module BWA
3
3
  class Configuration < Message
4
4
  MESSAGE_TYPE = "\xbf\x94".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 25
6
+
7
+ def inspect
8
+ "#<BWA::Messages::Configuration>"
9
+ end
6
10
  end
7
11
  end
8
12
  end
@@ -7,6 +7,7 @@ module BWA
7
7
  attr_accessor :model, :version
8
8
 
9
9
  def initialize
10
+ super
10
11
  @model = ''
11
12
  @version = 0
12
13
  end
@@ -7,14 +7,32 @@ module BWA
7
7
  attr_accessor :type
8
8
 
9
9
  def initialize(type = 1)
10
+ super()
10
11
  self.type = type
11
12
  end
12
13
 
13
14
  def parse(data)
14
- self.type = data == "\x02\x00\x00" ? 1 : 2
15
+ self.type = case data
16
+ when "\x02\x00\x00"; 1
17
+ when "\x00\x00\x01"; 2
18
+ when "\x01\x00\x00"; 3
19
+ else 0
20
+ end
15
21
  end
16
22
 
23
+ def serialize
24
+ data = case type
25
+ when 1; "\x02\x00\x00"
26
+ when 2; "\x00\x00\x01"
27
+ when 3; "\x01\x00\x00"
28
+ else "\x00\x00\x00"
29
+ end
30
+ super(data)
31
+ end
17
32
 
33
+ def inspect
34
+ "#<BWA::Messages::ControlConfigurationRequest #{type}>"
35
+ end
18
36
  end
19
37
  end
20
38
  end
@@ -3,6 +3,10 @@ module BWA
3
3
  class Ready < Message
4
4
  MESSAGE_TYPE = "\xbf\06".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 0
6
+
7
+ def inspect
8
+ "#<BWA::Messages::Ready>"
9
+ end
6
10
  end
7
11
  end
8
12
  end
@@ -7,6 +7,7 @@ module BWA
7
7
  attr_accessor :temperature
8
8
 
9
9
  def initialize(temperature = nil)
10
+ super()
10
11
  self.temperature = temperature
11
12
  end
12
13
 
@@ -7,6 +7,7 @@ module BWA
7
7
  attr_accessor :scale
8
8
 
9
9
  def initialize(scale = nil)
10
+ super()
10
11
  self.scale = scale
11
12
  end
12
13
 
@@ -7,6 +7,7 @@ module BWA
7
7
  attr_accessor :hour, :minute, :twenty_four_hour_time
8
8
 
9
9
  def initialize(hour = nil, minute = nil, twenty_four_hour_time = nil)
10
+ super()
10
11
  self.hour, self.minute, self.twenty_four_hour_time = hour, minute, twenty_four_hour_time
11
12
  end
12
13
 
@@ -1,7 +1,8 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class Status < Message
4
- attr_accessor :priming,
4
+ attr_accessor :hold,
5
+ :priming,
5
6
  :heating_mode,
6
7
  :temperature_scale,
7
8
  :twenty_four_hour_time,
@@ -23,6 +24,7 @@ module BWA
23
24
 
24
25
  def initialize
25
26
  @src = 0xff
27
+ self.hold = false
26
28
  self.priming = false
27
29
  self.heating_mode = :ready
28
30
  @temperature_scale = :fahrenheit
@@ -40,6 +42,9 @@ module BWA
40
42
  end
41
43
 
42
44
  def parse(data)
45
+ flags = data[0].ord
46
+ self.hold = (flags & 0x05 != 0 )
47
+
43
48
  flags = data[1].ord
44
49
  self.priming = (flags & 0x01 == 0x01)
45
50
  flags = data[5].ord
@@ -88,6 +93,7 @@ module BWA
88
93
 
89
94
  def serialize
90
95
  data = "\x00" * 24
96
+ data[0] = (hold ? 0x05 : 0x00).chr
91
97
  data[1] = (priming ? 0x01 : 0x00).chr
92
98
  data[5] = (case heating_mode
93
99
  when :ready; 0x00
@@ -118,7 +124,7 @@ module BWA
118
124
  data[2] = (current_temperature ? (current_temperature * 2).to_i : 0xff).chr
119
125
  data[20] = (set_temperature * 2).to_i.chr
120
126
  else
121
- data[2] = (current_temperature&.to_i || 0xff).chr
127
+ data[2] = (current_temperature.to_i || 0xff).chr
122
128
  data[20] = set_temperature.to_i.chr
123
129
  end
124
130
 
@@ -154,6 +160,7 @@ module BWA
154
160
  result = "#<BWA::Messages::Status "
155
161
  items = []
156
162
 
163
+ items << "hold" if hold
157
164
  items << "priming" if priming
158
165
  items << self.class.format_time(hour, minute, twenty_four_hour_time)
159
166
  items << "#{current_temperature || '--'}/#{set_temperature}º#{temperature_scale.to_s[0].upcase}"
@@ -7,6 +7,7 @@ module BWA
7
7
  attr_accessor :item
8
8
 
9
9
  def initialize(item = nil)
10
+ super()
10
11
  self.item = item
11
12
  end
12
13
 
@@ -14,6 +15,8 @@ module BWA
14
15
  self.item = case data[0].ord
15
16
  when 0x04; :pump1
16
17
  when 0x05; :pump2
18
+ when 0x0c; :blower
19
+ when 0x0e; :mister
17
20
  when 0x11; :light1
18
21
  when 0x3c; :hold
19
22
  when 0x50; :temperature_range
@@ -24,13 +27,20 @@ module BWA
24
27
 
25
28
  def serialize
26
29
  data = "\x00\x00"
27
- data[0] = (case setting
28
- when :pump1; 0x04
29
- when :pump2; 0x05
30
- when :light1; 0x11
31
- when :temperature_range; 0x50
32
- when :heating_mode; 0x51
33
- end).chr
30
+ if item.is_a? Integer
31
+ data[0] = item.chr
32
+ else
33
+ data[0] = (case item
34
+ when :pump1; 0x04
35
+ when :pump2; 0x05
36
+ when :blower; 0x0c
37
+ when :mister; 0x0e
38
+ when :light1; 0x11
39
+ when :hold; 0x3c
40
+ when :temperature_range; 0x50
41
+ when :heating_mode; 0x51
42
+ end).chr
43
+ end
34
44
  super(data)
35
45
  end
36
46
 
data/lib/bwa/proxy.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'bwa/logger'
2
3
  require 'bwa/message'
3
4
 
4
5
  module BWA
@@ -44,9 +45,9 @@ module BWA
44
45
  leftover_data = leftover_data[(data_length + 2)..-1] || ''
45
46
  begin
46
47
  message = Message.parse(data)
47
- puts "#{tag}: #{message.inspect}"
48
+ BWA.logger.info "#{tag}: #{message.inspect}"
48
49
  rescue InvalidMessage => e
49
- puts "#{tag}: #{e}"
50
+ BWA.logger.info "#{tag}: #{e}"
50
51
  end
51
52
  socket2.send(data, 0)
52
53
  end
data/lib/bwa/server.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'bwa/logger'
2
3
  require 'bwa/message'
3
4
 
4
5
  module BWA
@@ -26,7 +27,7 @@ module BWA
26
27
  end
27
28
 
28
29
  def run_client(socket)
29
- puts "Received connection from #{socket.remote_address.inspect}"
30
+ BWA.logger.info "Received connection from #{socket.remote_address.inspect}"
30
31
 
31
32
  send_status(socket)
32
33
  loop do
@@ -35,8 +36,8 @@ module BWA
35
36
  break if data.empty?
36
37
  begin
37
38
  message = Message.parse(data)
38
- puts message.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
39
- puts message.inspect
39
+ BWA.logger.info BWA.raw2str(message.raw_data)
40
+ BWA.logger.info message.inspect
40
41
 
41
42
  case message
42
43
  when Messages::ConfigurationRequest
@@ -64,8 +65,8 @@ module BWA
64
65
  end
65
66
  end
66
67
  rescue BWA::InvalidMessage => e
67
- puts e.message
68
- puts e.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
68
+ BWA.logger.warn e.message
69
+ BWA.logger.warn BWA.raw2str(e.raw_data)
69
70
  end
70
71
  else
71
72
  send_status(socket)
@@ -74,7 +75,7 @@ module BWA
74
75
  end
75
76
 
76
77
  def send_status(socket)
77
- puts "sending #{@status.inspect}"
78
+ BWA.logger.info "sending #{@status.inspect}"
78
79
  socket.send(@status.serialize, 0)
79
80
  end
80
81
 
data/lib/bwa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module BWA
2
- VERSION = '1.2.5'
2
+ VERSION = '1.3.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: balboa_worldwide_app
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.5
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-27 00:00:00.000000000 Z
11
+ date: 2021-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: digest-crc
@@ -123,6 +123,7 @@ files:
123
123
  - lib/bwa/client.rb
124
124
  - lib/bwa/crc.rb
125
125
  - lib/bwa/discovery.rb
126
+ - lib/bwa/logger.rb
126
127
  - lib/bwa/message.rb
127
128
  - lib/bwa/messages/configuration.rb
128
129
  - lib/bwa/messages/configuration_request.rb