balboa_worldwide_app 1.2.2 → 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 +4 -4
- data/bin/bwa_client +1 -1
- data/bin/bwa_mqtt_bridge +348 -9
- data/lib/bwa/client.rb +64 -21
- data/lib/bwa/discovery.rb +2 -1
- data/lib/bwa/logger.rb +55 -0
- data/lib/bwa/message.rb +52 -12
- data/lib/bwa/messages/configuration.rb +4 -0
- data/lib/bwa/messages/control_configuration.rb +1 -0
- data/lib/bwa/messages/control_configuration_request.rb +19 -1
- data/lib/bwa/messages/ready.rb +4 -0
- data/lib/bwa/messages/set_temperature.rb +1 -0
- data/lib/bwa/messages/set_temperature_scale.rb +1 -0
- data/lib/bwa/messages/set_time.rb +1 -0
- data/lib/bwa/messages/status.rb +11 -4
- data/lib/bwa/messages/toggle_item.rb +17 -7
- data/lib/bwa/proxy.rb +3 -2
- data/lib/bwa/server.rb +7 -6
- data/lib/bwa/version.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1f61d30556c0536efad6d27c229cefc0bccd1293a4dc491230a3e68cfddca13
|
4
|
+
data.tar.gz: 3014c3c575a53cceaa2f9c2903b1977480bffa5cc0db5e44f08bce8fc8671e17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ff3043cbbdefd778ee2fbea74ce46ad8c73860f8e541c9035911219b6d6048203f817a2f129bac16678334d4f02cddea806814357d13710c4c76fc3e1987b5e
|
7
|
+
data.tar.gz: bb1d5468380c06c7bc38c34cf984cd9c4550fb3a3f1314f1b85617df0cff003ece26ce4603746f59f0e00950ae8244aaf0f477149f3b693ab67b8f6ff30f8602
|
data/bin/bwa_client
CHANGED
data/bin/bwa_mqtt_bridge
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'mqtt'
|
4
|
+
require 'sd_notify'
|
5
|
+
require 'set'
|
6
|
+
require 'json'
|
4
7
|
|
8
|
+
require 'bwa/logger'
|
5
9
|
require 'bwa/client'
|
6
10
|
require 'bwa/discovery'
|
11
|
+
require 'bwa/version'
|
7
12
|
|
8
13
|
class MQTTBridge
|
9
14
|
def initialize(mqtt_uri, bwa, device_id: "bwa", base_topic: "homie")
|
@@ -15,7 +20,37 @@ class MQTTBridge
|
|
15
20
|
@attributes = {}
|
16
21
|
@things = Set.new
|
17
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
|
+
|
18
45
|
publish_basic_attributes
|
46
|
+
publish_filtercycles
|
47
|
+
|
48
|
+
# Home Assistant MQTT Discovery Section
|
49
|
+
publish_hass_discovery
|
50
|
+
|
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})"
|
53
|
+
SdNotify.ready
|
19
54
|
|
20
55
|
bwa_thread = Thread.new do
|
21
56
|
loop do
|
@@ -23,7 +58,6 @@ class MQTTBridge
|
|
23
58
|
message = @bwa.poll
|
24
59
|
next if message.is_a?(BWA::Messages::Ready)
|
25
60
|
|
26
|
-
puts message.inspect unless message.is_a?(BWA::Messages::Status)
|
27
61
|
case message
|
28
62
|
when BWA::Messages::ControlConfiguration
|
29
63
|
publish("spa/$type", message.model)
|
@@ -32,18 +66,29 @@ class MQTTBridge
|
|
32
66
|
publish_pump(i + 1, speed) if speed != 0
|
33
67
|
end
|
34
68
|
message.lights.each_with_index do |exists, i|
|
35
|
-
publish_thing(
|
69
|
+
publish_thing(:light, i + 1) if exists
|
36
70
|
end
|
37
71
|
message.aux.each_with_index do |exists, i|
|
38
|
-
publish_thing(
|
72
|
+
publish_thing(:aux, i + 1) if exists
|
39
73
|
end
|
40
74
|
publish_mister if message.mister
|
41
75
|
publish_blower(message.blower) if message.blower != 0
|
42
76
|
publish_circpump if message.circ_pump
|
43
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)
|
44
88
|
when BWA::Messages::Status
|
45
89
|
@bwa.request_control_info unless @bwa.last_control_configuration
|
46
90
|
@bwa.request_control_info2 unless @bwa.last_control_configuration2
|
91
|
+
@bwa.request_filter_configuration unless @bwa.last_filter_configuration
|
47
92
|
|
48
93
|
# make sure time is in sync
|
49
94
|
now = Time.now
|
@@ -54,8 +99,10 @@ class MQTTBridge
|
|
54
99
|
|
55
100
|
# allow a skew of 1 minute, since the seconds will always be off
|
56
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"
|
57
103
|
@bwa.set_time(now.hour, now.min, message.twenty_four_hour_time)
|
58
104
|
end
|
105
|
+
publish_attribute("spa/hold", message.hold)
|
59
106
|
publish_attribute("spa/priming", message.priming)
|
60
107
|
publish_attribute("spa/heatingmode", message.heating_mode)
|
61
108
|
publish_attribute("spa/temperaturescale", message.temperature_scale)
|
@@ -67,11 +114,13 @@ class MQTTBridge
|
|
67
114
|
publish_attribute("spa/settemperature", message.set_temperature)
|
68
115
|
publish_attribute("spa/settemperature/$unit", "º#{message.temperature_scale.to_s[0].upcase}")
|
69
116
|
if message.temperature_scale == :celsius
|
70
|
-
publish_attribute("spa/currenttemperature/$format",
|
117
|
+
publish_attribute("spa/currenttemperature/$format", "0:42")
|
71
118
|
publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "26:40" : "10:26")
|
119
|
+
publish_hass_discovery_settemp(:celsius)
|
72
120
|
else
|
73
|
-
publish_attribute("spa/currenttemperature/$format",
|
74
|
-
publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "80:
|
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)
|
75
124
|
end
|
76
125
|
publish_attribute("spa/filter1", message.filter[0])
|
77
126
|
publish_attribute("spa/filter2", message.filter[1])
|
@@ -88,14 +137,22 @@ class MQTTBridge
|
|
88
137
|
(0..1).each do |i|
|
89
138
|
publish_attribute("spa/aux#{i + 1}", message.lights[i]) if @bwa.last_control_configuration2&.aux&.[](i)
|
90
139
|
end
|
140
|
+
|
141
|
+
# Tell systemd we are still alive and kicking. Ignored if systemd not in use.
|
142
|
+
SdNotify.watchdog
|
143
|
+
|
91
144
|
end
|
92
145
|
end
|
93
146
|
end
|
94
147
|
end
|
95
148
|
|
96
149
|
@mqtt.get do |topic, value|
|
97
|
-
|
150
|
+
BWA.logger.warn "from mqtt: #{value.inspect} at #{topic}"
|
98
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')
|
99
156
|
when "spa/heatingmode/set"
|
100
157
|
next @bwa.toggle_heating_mode if value == 'toggle'
|
101
158
|
next unless %w{ready rest}.include?(value)
|
@@ -127,11 +184,14 @@ class MQTTBridge
|
|
127
184
|
@bwa.set_blower(value.to_i)
|
128
185
|
when "spa/settemperature/set"
|
129
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)
|
130
189
|
end
|
131
190
|
end
|
132
191
|
end
|
133
192
|
|
134
193
|
def publish(topic, value)
|
194
|
+
BWA.logger.debug " to mqtt: #{topic}: #{value}"
|
135
195
|
@mqtt.publish("#{@base_topic}/#{topic}", value, true)
|
136
196
|
end
|
137
197
|
|
@@ -146,8 +206,218 @@ class MQTTBridge
|
|
146
206
|
@mqtt.subscribe("#{@base_topic}/#{topic}")
|
147
207
|
end
|
148
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
|
+
|
149
419
|
def publish_basic_attributes
|
150
|
-
publish("$homie", "
|
420
|
+
publish("$homie", "4.0.0")
|
151
421
|
publish("$name", "BWA Spa")
|
152
422
|
publish("$state", "init")
|
153
423
|
publish("$nodes", "spa")
|
@@ -156,6 +426,11 @@ class MQTTBridge
|
|
156
426
|
publish("spa/$type", "spa")
|
157
427
|
publish_nodes
|
158
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
|
+
|
159
434
|
publish("spa/priming/$name", "Is the pump priming")
|
160
435
|
publish("spa/priming/$datatype", "boolean")
|
161
436
|
|
@@ -208,6 +483,7 @@ class MQTTBridge
|
|
208
483
|
subscribe("spa/pump#{i}/set")
|
209
484
|
|
210
485
|
@things << "pump#{i}"
|
486
|
+
publish_hass_discovery_pumps(i, speeds)
|
211
487
|
publish_nodes
|
212
488
|
end
|
213
489
|
|
@@ -218,6 +494,7 @@ class MQTTBridge
|
|
218
494
|
subscribe("spa/#{type}#{i}/set")
|
219
495
|
|
220
496
|
@things << "#{type}#{i}"
|
497
|
+
publish_hass_discovery_things(type, i)
|
221
498
|
publish_nodes
|
222
499
|
end
|
223
500
|
|
@@ -250,8 +527,68 @@ class MQTTBridge
|
|
250
527
|
publish_nodes
|
251
528
|
end
|
252
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
|
+
|
253
590
|
def publish_nodes
|
254
|
-
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(','))
|
255
592
|
end
|
256
593
|
end
|
257
594
|
|
@@ -260,6 +597,7 @@ mqtt_uri = ARGV.shift
|
|
260
597
|
if ARGV.empty?
|
261
598
|
spas = BWA::Discovery.discover
|
262
599
|
if spas.empty?
|
600
|
+
BWA.logger.fatal "Could not find spa!"
|
263
601
|
$stderr.puts "Could not find spa!"
|
264
602
|
exit 1
|
265
603
|
end
|
@@ -271,5 +609,6 @@ end
|
|
271
609
|
spa = BWA::Client.new(spa_ip)
|
272
610
|
|
273
611
|
spa.request_configuration
|
612
|
+
spa.request_filter_configuration
|
274
613
|
|
275
614
|
MQTTBridge.new(mqtt_uri, spa)
|
data/lib/bwa/client.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
require 'bwa/logger'
|
1
4
|
require 'bwa/message'
|
2
5
|
|
3
6
|
module BWA
|
@@ -8,7 +11,7 @@ module BWA
|
|
8
11
|
uri = URI.parse(uri)
|
9
12
|
if uri.scheme == 'tcp'
|
10
13
|
require 'socket'
|
11
|
-
@io = TCPSocket.new(uri.host, uri.port ||
|
14
|
+
@io = TCPSocket.new(uri.host, uri.port || 4257)
|
12
15
|
elsif uri.scheme == 'telnet' || uri.scheme == 'rfc2217'
|
13
16
|
require 'net/telnet/rfc2217'
|
14
17
|
@io = Net::Telnet::RFC2217.new("Host" => uri.host, "Port" => uri.port || 23, "baud" => 115200)
|
@@ -18,6 +21,7 @@ module BWA
|
|
18
21
|
@io = CCutrer::SerialPort.new(uri.path, baud: 115200)
|
19
22
|
@queue = []
|
20
23
|
end
|
24
|
+
@src = 0x0a
|
21
25
|
@buffer = ""
|
22
26
|
end
|
23
27
|
|
@@ -41,7 +45,7 @@ module BWA
|
|
41
45
|
end
|
42
46
|
|
43
47
|
if message.is_a?(Messages::Ready) && (msg = @queue&.shift)
|
44
|
-
|
48
|
+
BWA.logger.debug "wrote: #{BWA.raw2str(msg)}" unless BWA.verbosity < 1 && msg[3..4] == Messages::ControlConfigurationRequest::MESSAGE_TYPE
|
45
49
|
@io.write(msg)
|
46
50
|
end
|
47
51
|
@last_status = message.dup if message.is_a?(Messages::Status)
|
@@ -60,35 +64,35 @@ module BWA
|
|
60
64
|
end
|
61
65
|
|
62
66
|
def send_message(message)
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
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
|
67
70
|
if @queue
|
68
71
|
@queue.push(full_message)
|
69
72
|
else
|
73
|
+
BWA.logger.debug "wrote: #{BWA.raw2str(full_message)}" unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
|
70
74
|
@io.write(full_message)
|
71
75
|
end
|
72
76
|
end
|
73
77
|
|
74
78
|
def request_configuration
|
75
|
-
send_message(
|
79
|
+
send_message(Messages::ConfigurationRequest.new)
|
76
80
|
end
|
77
81
|
|
78
82
|
def request_control_info2
|
79
|
-
send_message(
|
83
|
+
send_message(Messages::ControlConfigurationRequest.new(2))
|
80
84
|
end
|
81
85
|
|
82
86
|
def request_control_info
|
83
|
-
send_message(
|
87
|
+
send_message(Messages::ControlConfigurationRequest.new(1))
|
84
88
|
end
|
85
89
|
|
86
90
|
def request_filter_configuration
|
87
|
-
send_message(
|
91
|
+
send_message(Messages::ControlConfigurationRequest.new(3))
|
88
92
|
end
|
89
93
|
|
90
94
|
def toggle_item(item)
|
91
|
-
send_message(
|
95
|
+
send_message(Messages::ToggleItem.new(item))
|
92
96
|
end
|
93
97
|
|
94
98
|
def toggle_pump(i)
|
@@ -100,11 +104,15 @@ module BWA
|
|
100
104
|
end
|
101
105
|
|
102
106
|
def toggle_mister
|
103
|
-
toggle_item(
|
107
|
+
toggle_item(:mister)
|
104
108
|
end
|
105
109
|
|
106
110
|
def toggle_blower
|
107
|
-
toggle_item(
|
111
|
+
toggle_item(:blower)
|
112
|
+
end
|
113
|
+
|
114
|
+
def toggle_hold
|
115
|
+
toggle_item(:hold)
|
108
116
|
end
|
109
117
|
|
110
118
|
def set_pump(i, desired)
|
@@ -141,22 +149,57 @@ module BWA
|
|
141
149
|
end
|
142
150
|
end
|
143
151
|
|
144
|
-
|
145
|
-
|
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)
|
146
160
|
def set_temperature(desired)
|
161
|
+
return unless last_status
|
162
|
+
return if last_status.set_temperature == desired
|
163
|
+
|
147
164
|
desired *= 2 if last_status && last_status.temperature_scale == :celsius || desired < 50
|
148
|
-
send_message(
|
165
|
+
send_message(Messages::SetTemperature.new(desired.round))
|
149
166
|
end
|
150
167
|
|
151
168
|
def set_time(hour, minute, twenty_four_hour_time = false)
|
152
|
-
hour
|
153
|
-
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))
|
154
170
|
end
|
155
171
|
|
156
172
|
def set_temperature_scale(scale)
|
157
173
|
raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I{fahrenheit :celsius}.include?(scale)
|
158
|
-
|
159
|
-
|
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
|
160
203
|
end
|
161
204
|
|
162
205
|
def toggle_temperature_range
|
@@ -170,7 +213,7 @@ module BWA
|
|
170
213
|
end
|
171
214
|
|
172
215
|
def toggle_heating_mode
|
173
|
-
toggle_item(
|
216
|
+
toggle_item(:heating_mode)
|
174
217
|
end
|
175
218
|
|
176
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
|
-
|
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,40 +23,75 @@ 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
|
|
61
|
+
# Keep scanning until message start char
|
30
62
|
next unless data[offset] == '~'
|
63
|
+
|
64
|
+
# Read length (safe since we have at least 5 chars)
|
31
65
|
length = data[offset + 1].ord
|
32
|
-
|
33
|
-
|
66
|
+
|
67
|
+
# No message is this short or this long; keep scanning
|
68
|
+
next if length < 5 or length >= '~'.ord
|
34
69
|
|
35
70
|
# don't have enough data for what this message wants;
|
36
|
-
#
|
37
|
-
|
71
|
+
# return and hope for more (yes this might cause a
|
72
|
+
# delay, but the protocol is very chatty so it won't
|
73
|
+
# be long)
|
74
|
+
return nil if length + 2 > data.length - offset
|
38
75
|
|
76
|
+
# Not properly terminated; keep scanning
|
39
77
|
next unless data[offset + length + 1] == '~'
|
40
78
|
|
79
|
+
# Not a valid checksum; keep scanning
|
41
80
|
next unless CRC.checksum(data.slice(offset + 1, length - 1)) == data[offset + length].ord
|
81
|
+
|
82
|
+
# Got a valid message!
|
42
83
|
break
|
43
84
|
end
|
44
85
|
|
45
|
-
|
46
|
-
|
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)
|
47
89
|
|
48
90
|
src = data[offset + 2].ord
|
49
|
-
message_type = data.slice(offset + 3, 2)
|
50
91
|
klass = @messages.find { |k| k::MESSAGE_TYPE == message_type }
|
51
92
|
|
52
|
-
|
53
|
-
return [nil, offset + length + 2] if
|
54
|
-
"\xbf\x00".force_encoding(Encoding::ASCII_8BIT),
|
55
|
-
"\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
|
56
|
-
"\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)
|
57
95
|
|
58
96
|
if klass
|
59
97
|
valid_length = if klass::MESSAGE_LENGTH.respond_to?(:include?)
|
@@ -63,6 +101,7 @@ module BWA
|
|
63
101
|
end
|
64
102
|
raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}", data) unless valid_length
|
65
103
|
else
|
104
|
+
BWA.logger.info "Unrecognized message type #{BWA.raw2str(message_type)}: #{BWA.raw2str(data.slice(offset, length + 2))}"
|
66
105
|
klass = Unrecognized
|
67
106
|
end
|
68
107
|
|
@@ -70,6 +109,7 @@ module BWA
|
|
70
109
|
message.parse(data.slice(offset + 5, length - 5))
|
71
110
|
message.instance_variable_set(:@raw_data, data.slice(offset, length + 2))
|
72
111
|
message.instance_variable_set(:@src, src)
|
112
|
+
BWA.logger.debug "from spa: #{message.inspect}" unless common_messages.include?(message_type)
|
73
113
|
[message, offset + length + 2]
|
74
114
|
end
|
75
115
|
|
@@ -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
|
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
|
data/lib/bwa/messages/ready.rb
CHANGED
data/lib/bwa/messages/status.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
module BWA
|
2
2
|
module Messages
|
3
3
|
class Status < Message
|
4
|
-
attr_accessor :
|
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
|
@@ -81,13 +86,14 @@ module BWA
|
|
81
86
|
self.current_temperature = nil if self.current_temperature == 0xff
|
82
87
|
self.set_temperature = data[20].ord
|
83
88
|
if temperature_scale == :celsius
|
84
|
-
self.current_temperature /= 2.0
|
85
|
-
self.set_temperature /= 2.0
|
89
|
+
self.current_temperature /= 2.0 if self.current_temperature
|
90
|
+
self.set_temperature /= 2.0 if self.set_temperature
|
86
91
|
end
|
87
92
|
end
|
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
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
48
|
+
BWA.logger.info "#{tag}: #{message.inspect}"
|
48
49
|
rescue InvalidMessage => e
|
49
|
-
|
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
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
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
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.
|
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-
|
11
|
+
date: 2021-11-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: digest-crc
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 1.0.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sd_notify
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.1.1
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.1.1
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: byebug
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -109,6 +123,7 @@ files:
|
|
109
123
|
- lib/bwa/client.rb
|
110
124
|
- lib/bwa/crc.rb
|
111
125
|
- lib/bwa/discovery.rb
|
126
|
+
- lib/bwa/logger.rb
|
112
127
|
- lib/bwa/message.rb
|
113
128
|
- lib/bwa/messages/configuration.rb
|
114
129
|
- lib/bwa/messages/configuration_request.rb
|
@@ -143,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
158
|
- !ruby/object:Gem::Version
|
144
159
|
version: '0'
|
145
160
|
requirements: []
|
146
|
-
rubygems_version: 3.
|
161
|
+
rubygems_version: 3.1.2
|
147
162
|
signing_key:
|
148
163
|
specification_version: 4
|
149
164
|
summary: Library for communication with Balboa Water Group's WiFi module or RS-485
|