balboa_worldwide_app 1.2.5 → 1.3.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: 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