balboa_worldwide_app 1.0.0 → 1.2.1
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_mqtt_bridge +126 -39
- data/lib/bwa/client.rb +112 -37
- data/lib/bwa/message.rb +50 -44
- data/lib/bwa/messages/configuration.rb +1 -1
- data/lib/bwa/messages/configuration_request.rb +1 -1
- data/lib/bwa/messages/control_configuration.rb +64 -6
- data/lib/bwa/messages/control_configuration_request.rb +1 -1
- data/lib/bwa/messages/filter_cycles.rb +1 -1
- data/lib/bwa/messages/ready.rb +1 -1
- data/lib/bwa/messages/set_temperature.rb +1 -1
- data/lib/bwa/messages/set_temperature_scale.rb +1 -1
- data/lib/bwa/messages/set_time.rb +1 -1
- data/lib/bwa/messages/status.rb +39 -13
- data/lib/bwa/messages/toggle_item.rb +3 -1
- data/lib/bwa/version.rb +2 -2
- metadata +27 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 537f4b0048b2f972b06f90fc404fc9589845cc7550e5305a19e0bab892e647e4
|
4
|
+
data.tar.gz: 79fd85b3e8e95b6fade48c8135c170a52956b60d69199a84a31bed4ccd6f7c61
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9517634786bc75db423f2a2b4e2941535fc31deb0dd38f610c94f613ce4d00eafeadcc43c517fd5f475fc62392d00ba42b57f8b0abaad9d18289e4bf65fa1a7
|
7
|
+
data.tar.gz: 7d4388cdeefa5477402594681fad8fee169ca02dc6449edec60cd827f80e81d9aba53251202217c6e8c20feca8e0ab29b20b4703d04641259a9eddeb47fc3d1c
|
data/bin/bwa_mqtt_bridge
CHANGED
@@ -13,6 +13,7 @@ class MQTTBridge
|
|
13
13
|
@mqtt.connect
|
14
14
|
@bwa = bwa
|
15
15
|
@attributes = {}
|
16
|
+
@things = Set.new
|
16
17
|
|
17
18
|
publish_basic_attributes
|
18
19
|
|
@@ -22,11 +23,37 @@ class MQTTBridge
|
|
22
23
|
message = @bwa.poll
|
23
24
|
next if message.is_a?(BWA::Messages::Ready)
|
24
25
|
|
26
|
+
puts message.inspect unless message.is_a?(BWA::Messages::Status)
|
25
27
|
case message
|
28
|
+
when BWA::Messages::ControlConfiguration
|
29
|
+
publish("spa/$type", message.model)
|
30
|
+
when BWA::Messages::ControlConfiguration2
|
31
|
+
message.pumps.each_with_index do |speed, i|
|
32
|
+
publish_pump(i + 1, speed) if speed != 0
|
33
|
+
end
|
34
|
+
message.lights.each_with_index do |exists, i|
|
35
|
+
publish_thing("light", i + 1) if exists
|
36
|
+
end
|
37
|
+
message.aux.each_with_index do |exists, i|
|
38
|
+
publish_thing("aux", i + 1) if exists
|
39
|
+
end
|
40
|
+
publish_mister if message.mister
|
41
|
+
publish_blower(message.blower) if message.blower != 0
|
42
|
+
publish_circpump if message.circ_pump
|
43
|
+
publish("$state", "ready")
|
26
44
|
when BWA::Messages::Status
|
45
|
+
@bwa.request_control_info unless @bwa.last_control_configuration
|
46
|
+
@bwa.request_control_info2 unless @bwa.last_control_configuration2
|
47
|
+
|
27
48
|
# make sure time is in sync
|
28
49
|
now = Time.now
|
29
|
-
|
50
|
+
now_minutes = now.hour * 60 + now.min
|
51
|
+
spa_minutes = message.hour * 60 + message.minute
|
52
|
+
# check the difference in both directions
|
53
|
+
diff = [(spa_minutes - now_minutes) % 1440, 1440 - (spa_minutes - now_minutes) % 1440].min
|
54
|
+
|
55
|
+
# allow a skew of 1 minute, since the seconds will always be off
|
56
|
+
if diff > 1
|
30
57
|
@bwa.set_time(now.hour, now.min, message.twenty_four_hour_time)
|
31
58
|
end
|
32
59
|
publish_attribute("spa/priming", message.priming)
|
@@ -35,10 +62,6 @@ class MQTTBridge
|
|
35
62
|
publish_attribute("spa/24htime", message.twenty_four_hour_time)
|
36
63
|
publish_attribute("spa/heating", message.heating)
|
37
64
|
publish_attribute("spa/temperaturerange", message.temperature_range)
|
38
|
-
publish_attribute("spa/circpump", message.circ_pump)
|
39
|
-
publish_attribute("spa/pump1", message.pump1)
|
40
|
-
publish_attribute("spa/pump2", message.pump2)
|
41
|
-
publish_attribute("spa/light1", message.light1)
|
42
65
|
publish_attribute("spa/currenttemperature", message.current_temperature)
|
43
66
|
publish_attribute("spa/currenttemperature/$unit", "º#{message.temperature_scale.to_s[0].upcase}")
|
44
67
|
publish_attribute("spa/settemperature", message.set_temperature)
|
@@ -50,6 +73,21 @@ class MQTTBridge
|
|
50
73
|
publish_attribute("spa/currenttemperature/$format", message.temperature_range == :high ? "80:104" : "26:40")
|
51
74
|
publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "80:104" : "26:40")
|
52
75
|
end
|
76
|
+
publish_attribute("spa/filter1", message.filter[0])
|
77
|
+
publish_attribute("spa/filter2", message.filter[1])
|
78
|
+
|
79
|
+
publish_attribute("spa/circpump", message.circ_pump) if @bwa.last_control_configuration2&.circ_pump
|
80
|
+
publish_attribute("spa/blower", message.blower) if @bwa.last_control_configuration2&.blower.to_i != 0
|
81
|
+
publish_attribute("spa/mister", message.mister) if @bwa.last_control_configuration2&.mister
|
82
|
+
(0..5).each do |i|
|
83
|
+
publish_attribute("spa/pump#{i + 1}", message.pumps[i]) if @bwa.last_control_configuration2&.pumps&.[](i).to_i != 0
|
84
|
+
end
|
85
|
+
(0..1).each do |i|
|
86
|
+
publish_attribute("spa/light#{i + 1}", message.lights[i]) if @bwa.last_control_configuration2&.lights&.[](i)
|
87
|
+
end
|
88
|
+
(0..1).each do |i|
|
89
|
+
publish_attribute("spa/aux#{i + 1}", message.lights[i]) if @bwa.last_control_configuration2&.aux&.[](i)
|
90
|
+
end
|
53
91
|
end
|
54
92
|
end
|
55
93
|
end
|
@@ -58,24 +96,35 @@ class MQTTBridge
|
|
58
96
|
@mqtt.get do |topic, value|
|
59
97
|
puts "got #{value.inspect} at #{topic}"
|
60
98
|
case topic[@base_topic.length + 1..-1]
|
61
|
-
when "heatingmode"
|
62
|
-
next
|
63
|
-
|
64
|
-
|
99
|
+
when "spa/heatingmode/set"
|
100
|
+
next @bwa.toggle_heating_mode if value == 'toggle'
|
101
|
+
next unless %w{ready rest}.include?(value)
|
102
|
+
@bwa.set_heating_mode(value.to_sym)
|
103
|
+
when "spa/temperaturescale/set"
|
65
104
|
next unless %w{fahrenheit celsius}.include?(value)
|
66
|
-
|
105
|
+
@bwa.set_temperature_scale(value.to_sym)
|
67
106
|
when "spa/24htime/set"
|
68
107
|
next unless %w{true false}.include?(value)
|
69
108
|
now = Time.now
|
70
109
|
@bwa.set_time(now.hour, now.min, value == 'true')
|
71
|
-
when "temperaturerange"
|
110
|
+
when "spa/temperaturerange/set"
|
111
|
+
next @bwa.toggle_temperature_range if value == 'toggle'
|
72
112
|
next unless %w{low high}.include?(value)
|
73
|
-
|
74
|
-
when %r{^spa/(
|
75
|
-
@bwa.
|
76
|
-
|
113
|
+
@bwa.set_temperature_range(value.to_sym)
|
114
|
+
when %r{^spa/pump([1-6])/set$}
|
115
|
+
next @bwa.toggle_pump($1.to_i) if value == 'toggle'
|
116
|
+
@bwa.set_pump($1.to_i, value.to_i)
|
117
|
+
when %r{^spa/(light|aux)([12])/set$}
|
118
|
+
next @bwa.send(:"toggle_#{$1}", $2.to_i) if value == 'toggle'
|
77
119
|
next unless %w{true false}.include?(value)
|
78
|
-
@bwa.
|
120
|
+
@bwa.send(:"set_#{$1}", $2.to_i, value == 'true')
|
121
|
+
when "spa/mister/set"
|
122
|
+
next @bwa.toggle_mister if value == 'toggle'
|
123
|
+
next unless %w{true false}.include?(value)
|
124
|
+
@bwa.set_mister(value == 'true')
|
125
|
+
when "spa/blower/set"
|
126
|
+
next @bwa.toggle_blower if value == 'toggle'
|
127
|
+
@bwa.set_blower(value.to_i)
|
79
128
|
when "spa/settemperature/set"
|
80
129
|
@bwa.set_temperature(value.to_i)
|
81
130
|
end
|
@@ -105,7 +154,7 @@ class MQTTBridge
|
|
105
154
|
|
106
155
|
publish("spa/$name", "BWA Spa")
|
107
156
|
publish("spa/$type", "spa")
|
108
|
-
|
157
|
+
publish_nodes
|
109
158
|
|
110
159
|
publish("spa/priming/$name", "Is the pump priming")
|
111
160
|
publish("spa/priming/$datatype", "boolean")
|
@@ -136,26 +185,6 @@ class MQTTBridge
|
|
136
185
|
publish("spa/temperaturerange/$settable", "true")
|
137
186
|
subscribe("spa/temperaturerange/set")
|
138
187
|
|
139
|
-
publish("spa/circpump/$name", "Circ pump is currently running")
|
140
|
-
publish("spa/circpump/$datatype", "boolean")
|
141
|
-
|
142
|
-
publish("spa/pump1/$name", "Pump 1 speed")
|
143
|
-
publish("spa/pump1/$datatype", "integer")
|
144
|
-
publish("spa/pump1/$format", "0:2")
|
145
|
-
publish("spa/pump1/$settable", "true")
|
146
|
-
subscribe("spa/pump1/set")
|
147
|
-
|
148
|
-
publish("spa/pump2/$name", "Pump 2 speed")
|
149
|
-
publish("spa/pump2/$datatype", "integer")
|
150
|
-
publish("spa/pump2/$format", "0:2")
|
151
|
-
publish("spa/pump2/$settable", "true")
|
152
|
-
subscribe("spa/pump2/set")
|
153
|
-
|
154
|
-
publish("spa/light1/$name", "Light 1")
|
155
|
-
publish("spa/light1/$datatype", "boolean")
|
156
|
-
publish("spa/light1/$settable", "true")
|
157
|
-
subscribe("spa/light1/set")
|
158
|
-
|
159
188
|
publish("spa/currenttemperature/$name", "Current temperature")
|
160
189
|
publish("spa/currenttemperature/$datatype", "integer")
|
161
190
|
|
@@ -164,7 +193,65 @@ class MQTTBridge
|
|
164
193
|
publish("spa/settemperature/$settable", "true")
|
165
194
|
subscribe("spa/settemperature/set")
|
166
195
|
|
167
|
-
publish("
|
196
|
+
publish("spa/filter1/$name", "Filter cycle 1 is currently running")
|
197
|
+
publish("spa/filter1/$datatype", "boolean")
|
198
|
+
|
199
|
+
publish("spa/filter2/$name", "Filter cycle 2 is currently running")
|
200
|
+
publish("spa/filter2/$datatype", "boolean")
|
201
|
+
end
|
202
|
+
|
203
|
+
def publish_pump(i, speeds)
|
204
|
+
publish("spa/pump#{i}/$name", "Pump #{i} speed")
|
205
|
+
publish("spa/pump#{i}/$datatype", "integer")
|
206
|
+
publish("spa/pump#{i}/$format", "0:#{speeds}")
|
207
|
+
publish("spa/pump#{i}/$settable", "true")
|
208
|
+
subscribe("spa/pump#{i}/set")
|
209
|
+
|
210
|
+
@things << "pump#{i}"
|
211
|
+
publish_nodes
|
212
|
+
end
|
213
|
+
|
214
|
+
def publish_thing(type, i)
|
215
|
+
publish("spa/#{type}#{i}/$name", "#{type} #{i}")
|
216
|
+
publish("spa/#{type}#{i}/$datatype", "boolean")
|
217
|
+
publish("spa/#{type}#{i}/$settable", "true")
|
218
|
+
subscribe("spa/#{type}#{i}/set")
|
219
|
+
|
220
|
+
@things << "#{type}#{i}"
|
221
|
+
publish_nodes
|
222
|
+
end
|
223
|
+
|
224
|
+
def publish_mister
|
225
|
+
publish("spa/mister/$name", type)
|
226
|
+
publish("spa/mister/$datatype", "boolean")
|
227
|
+
publish("spa/mister/$settable", "true")
|
228
|
+
subscribe("spa/mister/set")
|
229
|
+
|
230
|
+
@things << "mister"
|
231
|
+
publish_nodes
|
232
|
+
end
|
233
|
+
|
234
|
+
def publish_blower(speeds)
|
235
|
+
publish("spa/blower/$name", "Blower speed")
|
236
|
+
publish("spa/blower/$datatype", "integer")
|
237
|
+
publish("spa/blower/$format", "0:#{speeds}")
|
238
|
+
publish("spa/blower/$settable", "true")
|
239
|
+
subscribe("spa/blower/set")
|
240
|
+
|
241
|
+
@things << "blower"
|
242
|
+
publish_nodes
|
243
|
+
end
|
244
|
+
|
245
|
+
def publish_circpump
|
246
|
+
publish("spa/circpump/$name", "Circ pump is currently running")
|
247
|
+
publish("spa/circpump/$datatype", "boolean")
|
248
|
+
@things << "circpump"
|
249
|
+
|
250
|
+
publish_nodes
|
251
|
+
end
|
252
|
+
|
253
|
+
def publish_nodes
|
254
|
+
publish("spa/$properties", (["priming,heatingmode,temperaturescale,24htime,heating,temperaturerange,currenttemperature,settemperature,filter1,filter2"] + @things.to_a).join(','))
|
168
255
|
end
|
169
256
|
end
|
170
257
|
|
@@ -176,7 +263,7 @@ if ARGV.empty?
|
|
176
263
|
$stderr.puts "Could not find spa!"
|
177
264
|
exit 1
|
178
265
|
end
|
179
|
-
spa_ip = spas.first.first
|
266
|
+
spa_ip = "tcp://#{spas.first.first}/"
|
180
267
|
else
|
181
268
|
spa_ip = ARGV[0]
|
182
269
|
end
|
data/lib/bwa/client.rb
CHANGED
@@ -2,36 +2,52 @@ require 'bwa/message'
|
|
2
2
|
|
3
3
|
module BWA
|
4
4
|
class Client
|
5
|
-
attr_reader :last_status, :last_filter_configuration
|
5
|
+
attr_reader :last_status, :last_control_configuration, :last_control_configuration2, :last_filter_configuration
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def initialize(uri)
|
8
|
+
uri = URI.parse(uri)
|
9
|
+
if uri.scheme == 'tcp'
|
10
|
+
require 'socket'
|
11
|
+
@io = TCPSocket.new(uri.host, uri.port || 4217)
|
12
|
+
elsif uri.scheme == 'telnet' || uri.scheme == 'rfc2217'
|
13
|
+
require 'net/telnet/rfc2217'
|
14
|
+
@io = Net::Telnet::RFC2217.new("Host" => uri.host, "Port" => uri.port || 23, "baud" => 115200)
|
11
15
|
@queue = []
|
12
16
|
else
|
13
|
-
require '
|
14
|
-
@io =
|
17
|
+
require 'ccutrer-serialport'
|
18
|
+
@io = CCutrer::SerialPort.new(uri.path, baud: 115200)
|
19
|
+
@queue = []
|
15
20
|
end
|
21
|
+
@buffer = ""
|
16
22
|
end
|
17
23
|
|
18
24
|
def poll
|
19
|
-
message = nil
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
message = bytes_read = nil
|
26
|
+
loop do
|
27
|
+
message, bytes_read = Message.parse(@buffer)
|
28
|
+
# discard how much we read
|
29
|
+
@buffer = @buffer[bytes_read..-1] if bytes_read
|
30
|
+
method = @io.respond_to?(:readpartial) ? :readpartial : :read
|
31
|
+
unless message
|
32
|
+
begin
|
33
|
+
@buffer.concat(@io.__send__(method, 64 * 1024))
|
34
|
+
rescue EOFError
|
35
|
+
@io.wait_readable
|
36
|
+
retry
|
30
37
|
end
|
38
|
+
next
|
31
39
|
end
|
40
|
+
break
|
41
|
+
end
|
42
|
+
|
43
|
+
if message.is_a?(Messages::Ready) && (msg = @queue&.shift)
|
44
|
+
puts "wrote #{msg.unpack('H*').first}"
|
45
|
+
@io.write(msg)
|
32
46
|
end
|
33
47
|
@last_status = message.dup if message.is_a?(Messages::Status)
|
34
48
|
@last_filter_configuration = message.dup if message.is_a?(Messages::FilterCycles)
|
49
|
+
@last_control_configuration = message.dup if message.is_a?(Messages::ControlConfiguration)
|
50
|
+
@last_control_configuration2 = message.dup if message.is_a?(Messages::ControlConfiguration2)
|
35
51
|
message
|
36
52
|
end
|
37
53
|
|
@@ -59,6 +75,10 @@ module BWA
|
|
59
75
|
send_message("\x0a\xbf\x04")
|
60
76
|
end
|
61
77
|
|
78
|
+
def request_control_info2
|
79
|
+
send_message("\x0a\xbf\x22\x00\x00\x01")
|
80
|
+
end
|
81
|
+
|
62
82
|
def request_control_info
|
63
83
|
send_message("\x0a\xbf\x22\x02\x00\x00")
|
64
84
|
end
|
@@ -67,39 +87,58 @@ module BWA
|
|
67
87
|
send_message("\x0a\xbf\x22\x01\x00\x00")
|
68
88
|
end
|
69
89
|
|
70
|
-
def toggle_item(
|
71
|
-
send_message("\x0a\xbf\x11#{
|
90
|
+
def toggle_item(item)
|
91
|
+
send_message("\x0a\xbf\x11#{item.chr}\x00")
|
72
92
|
end
|
73
93
|
|
74
|
-
def
|
75
|
-
toggle_item(
|
94
|
+
def toggle_pump(i)
|
95
|
+
toggle_item(i + 3)
|
76
96
|
end
|
77
97
|
|
78
|
-
def
|
79
|
-
toggle_item(
|
98
|
+
def toggle_light(i)
|
99
|
+
toggle_item(i + 0x10)
|
80
100
|
end
|
81
101
|
|
82
|
-
def
|
83
|
-
toggle_item(
|
102
|
+
def toggle_mister
|
103
|
+
toggle_item(0x0e)
|
84
104
|
end
|
85
105
|
|
86
|
-
|
106
|
+
def toggle_blower
|
107
|
+
toggle_item(0x0c)
|
108
|
+
end
|
109
|
+
|
110
|
+
def set_pump(i, desired)
|
111
|
+
return unless last_status && last_control_configuration2
|
112
|
+
times = (desired - last_status.pumps[i - 1]) % (last_control_configuration2.pumps[i - 1] + 1)
|
113
|
+
times.times do
|
114
|
+
toggle_pump(i)
|
115
|
+
sleep(0.1)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
%w{light aux}.each do |type|
|
87
120
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
toggle_pump#{i}
|
93
|
-
sleep(0.1)
|
121
|
+
def set_#{type}(i, desired)
|
122
|
+
return unless last_status
|
123
|
+
return if last_status.#{type}s[i - 1] == desired
|
124
|
+
toggle_#{type}(i)
|
94
125
|
end
|
95
|
-
end
|
96
126
|
RUBY
|
97
127
|
end
|
98
128
|
|
99
|
-
def
|
129
|
+
def set_mister(desired)
|
100
130
|
return unless last_status
|
101
|
-
return if last_status.
|
102
|
-
|
131
|
+
return if last_status.mister == desired
|
132
|
+
toggle_mister
|
133
|
+
end
|
134
|
+
|
135
|
+
def set_blower(desired)
|
136
|
+
return unless last_status && last_control_configuration2
|
137
|
+
times = (desired - last_status.blower) % (last_control_configuration2.blower + 1)
|
138
|
+
times.times do
|
139
|
+
toggle_blower
|
140
|
+
sleep(0.1)
|
141
|
+
end
|
103
142
|
end
|
104
143
|
|
105
144
|
# high range is 80-104 for F, 26-40 for C (by 0.5)
|
@@ -113,5 +152,41 @@ module BWA
|
|
113
152
|
hour |= 0x80 if twenty_four_hour_time
|
114
153
|
send_message("\x0a\xbf\x21".force_encoding(Encoding::ASCII_8BIT) + hour.chr + minute.chr)
|
115
154
|
end
|
155
|
+
|
156
|
+
def set_temperature_scale(scale)
|
157
|
+
raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I{fahrenheit :celsius}.include?(scale)
|
158
|
+
arg = scale == :fahrenheit ? 0 : 1
|
159
|
+
send_message("\x0a\xbf\x27\x01".force_encoding(Encoding::ASCII_8BIT) + arg.chr)
|
160
|
+
end
|
161
|
+
|
162
|
+
def toggle_temperature_range
|
163
|
+
toggle_item(0x50)
|
164
|
+
end
|
165
|
+
|
166
|
+
def set_temperature_range(desired)
|
167
|
+
return unless last_status
|
168
|
+
return if last_status.temperature_range == desired
|
169
|
+
toggle_temperature_range
|
170
|
+
end
|
171
|
+
|
172
|
+
def toggle_heating_mode
|
173
|
+
toggle_item(0x51)
|
174
|
+
end
|
175
|
+
|
176
|
+
HEATING_MODES = %I{ready rest ready_in_rest}.freeze
|
177
|
+
def set_heating_mode(desired)
|
178
|
+
raise ArgumentError, "heating_mode must be :ready or :rest" unless %I{ready rest}.include?(desired)
|
179
|
+
return unless last_status
|
180
|
+
times = if last_status.heating_mode == :ready && desired == :rest ||
|
181
|
+
last_status.heating_mode == :rest && desired == :ready ||
|
182
|
+
last_status.heating_mode == :ready_in_rest && desired == :rest
|
183
|
+
1
|
184
|
+
elsif last_status.heating_mode == :ready_in_rest && desired == :ready
|
185
|
+
2
|
186
|
+
else
|
187
|
+
0
|
188
|
+
end
|
189
|
+
times.times { toggle_heating_mode }
|
190
|
+
end
|
116
191
|
end
|
117
192
|
end
|
data/lib/bwa/message.rb
CHANGED
@@ -11,65 +11,66 @@ module BWA
|
|
11
11
|
end
|
12
12
|
|
13
13
|
class Message
|
14
|
+
class Unrecognized < Message
|
15
|
+
end
|
16
|
+
|
14
17
|
class << self
|
15
18
|
def inherited(klass)
|
16
19
|
@messages ||= []
|
17
20
|
@messages << klass
|
18
21
|
end
|
19
22
|
|
20
|
-
def parse(
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
data
|
26
|
-
end
|
23
|
+
def parse(data)
|
24
|
+
offset = -1
|
25
|
+
message_type = length = message_class = nil
|
26
|
+
loop do
|
27
|
+
offset += 1
|
28
|
+
return nil if data.length - offset < 5
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
+
next unless data[offset] == '~'
|
31
|
+
length = data[offset + 1].ord
|
32
|
+
# impossible message
|
33
|
+
next if length < 5
|
30
34
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
bytes.reverse.each { |byte| io.ungetbyte(byte) }
|
35
|
-
raise InvalidMessage.new("Message has bogus length: #{length}", data)
|
36
|
-
end
|
35
|
+
# don't have enough data for what this message wants;
|
36
|
+
# it could be garbage on the line so keep scanning
|
37
|
+
next if length + 2 > data.length - offset
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
data.
|
41
|
-
|
39
|
+
next unless data[offset + length + 1] == '~'
|
40
|
+
|
41
|
+
next unless CRC.checksum(data.slice(offset + 1, length - 1)) == data[offset + length].ord
|
42
|
+
break
|
42
43
|
end
|
43
44
|
|
44
|
-
|
45
|
-
|
45
|
+
puts "discarding invalid data prior to message #{data[0...offset].unpack('H*').first}" unless offset == 0
|
46
|
+
#puts "read #{data.slice(offset, length + 2).unpack('H*').first}"
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
bytes.reverse.each { |b| io.ungetbyte(b) }
|
51
|
-
raise InvalidMessage.new("Missing trailing message indicator", data)
|
52
|
-
end
|
48
|
+
src = data[offset + 2].ord
|
49
|
+
message_type = data.slice(offset + 3, 2)
|
50
|
+
klass = @messages.find { |k| k::MESSAGE_TYPE == message_type }
|
53
51
|
|
54
|
-
unless CRC.checksum(data[1...-2]) == data[-2].ord
|
55
|
-
bytes = data.bytes
|
56
|
-
bytes.shift
|
57
|
-
bytes.reverse.each { |b| io.ungetbyte(b) }
|
58
|
-
raise InvalidMessage.new("Invalid checksum", data)
|
59
|
-
end
|
60
52
|
|
61
|
-
return nil if [
|
62
|
-
"\
|
63
|
-
"\
|
64
|
-
"\
|
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)
|
65
57
|
|
66
|
-
|
67
|
-
|
58
|
+
if klass
|
59
|
+
valid_length = if klass::MESSAGE_LENGTH.respond_to?(:include?)
|
60
|
+
klass::MESSAGE_LENGTH.include?(length - 5)
|
61
|
+
else
|
62
|
+
length - 5 == klass::MESSAGE_LENGTH
|
63
|
+
end
|
64
|
+
raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}", data) unless valid_length
|
65
|
+
else
|
66
|
+
klass = Unrecognized
|
67
|
+
end
|
68
68
|
|
69
69
|
message = klass.new
|
70
|
-
message.parse(data
|
71
|
-
message.instance_variable_set(:@raw_data, data)
|
72
|
-
message
|
70
|
+
message.parse(data.slice(offset + 5, length - 5))
|
71
|
+
message.instance_variable_set(:@raw_data, data.slice(offset, length + 2))
|
72
|
+
message.instance_variable_set(:@src, src)
|
73
|
+
[message, offset + length + 2]
|
73
74
|
end
|
74
75
|
|
75
76
|
def format_time(hour, minute, twenty_four_hour_time = true)
|
@@ -88,14 +89,19 @@ module BWA
|
|
88
89
|
end
|
89
90
|
end
|
90
91
|
|
91
|
-
attr_reader :raw_data
|
92
|
+
attr_reader :raw_data, :src
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
# most messages we're sending come from this address
|
96
|
+
@src = 0x0a
|
97
|
+
end
|
92
98
|
|
93
99
|
def parse(_data)
|
94
100
|
end
|
95
101
|
|
96
102
|
def serialize(message = "")
|
97
103
|
length = message.length + 5
|
98
|
-
full_message = "#{length.chr}#{self.class::MESSAGE_TYPE}#{message}".force_encoding(Encoding::ASCII_8BIT)
|
104
|
+
full_message = "#{length.chr}#{src.chr}#{self.class::MESSAGE_TYPE}#{message}".force_encoding(Encoding::ASCII_8BIT)
|
99
105
|
checksum = CRC.checksum(full_message)
|
100
106
|
"\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT)
|
101
107
|
end
|
@@ -1,17 +1,75 @@
|
|
1
1
|
module BWA
|
2
2
|
module Messages
|
3
3
|
class ControlConfiguration < Message
|
4
|
-
MESSAGE_TYPE = "\
|
4
|
+
MESSAGE_TYPE = "\xbf\x24".force_encoding(Encoding::ASCII_8BIT)
|
5
5
|
MESSAGE_LENGTH = 21
|
6
|
+
|
7
|
+
attr_accessor :model, :version
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@model = ''
|
11
|
+
@version = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(data)
|
15
|
+
self.version = "V#{data[2].ord}.#{data[3].ord}"
|
16
|
+
self.model = data[4..11].strip
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
"#<BWA::Messages::ControlConfiguration #{model} #{version}>"
|
21
|
+
end
|
6
22
|
end
|
7
|
-
end
|
8
|
-
end
|
9
23
|
|
10
|
-
module BWA
|
11
|
-
module Messages
|
12
24
|
class ControlConfiguration2 < Message
|
13
|
-
MESSAGE_TYPE = "\
|
25
|
+
MESSAGE_TYPE = "\xbf\x2e".force_encoding(Encoding::ASCII_8BIT)
|
14
26
|
MESSAGE_LENGTH = 6
|
27
|
+
|
28
|
+
attr_accessor :pumps, :lights, :circ_pump, :blower, :mister, :aux
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
self.pumps = Array.new(6, 0)
|
32
|
+
self.lights = Array.new(2, false)
|
33
|
+
self.circ_pump = false
|
34
|
+
self.blower = 0
|
35
|
+
self.mister = false
|
36
|
+
self.aux = Array.new(2, false)
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse(data)
|
40
|
+
flags = data[0].ord
|
41
|
+
pumps[0] = flags & 0x03
|
42
|
+
pumps[1] = (flags >> 2) & 0x03
|
43
|
+
pumps[2] = (flags >> 4) & 0x03
|
44
|
+
pumps[3] = (flags >> 6) & 0x03
|
45
|
+
flags = data[1].ord
|
46
|
+
pumps[4] = flags & 0x03
|
47
|
+
pumps[5] = (flags >> 6) & 0x03
|
48
|
+
flags = data[2].ord
|
49
|
+
lights[0] = (flags & 0x03 != 0)
|
50
|
+
lights[1] = ((flags >> 6) & 0x03 != 0)
|
51
|
+
flags = data[3].ord
|
52
|
+
self.blower = flags & 0x03
|
53
|
+
self.circ_pump = ((flags >> 6) & 0x03 != 0)
|
54
|
+
flags = data[4].ord
|
55
|
+
self.mister = (flags & 0x30 != 0)
|
56
|
+
aux[0] = (flags & 0x01 != 0)
|
57
|
+
aux[1] = (flags & 0x02 != 0)
|
58
|
+
end
|
59
|
+
|
60
|
+
def inspect
|
61
|
+
result = "#<BWA::Messages::ControlConfiguration2 "
|
62
|
+
items = []
|
63
|
+
|
64
|
+
items << "pumps=#{pumps.inspect}"
|
65
|
+
items << "lights=#{lights.inspect}"
|
66
|
+
items << "circ_pump" if circ_pump
|
67
|
+
items << "blower=#{blower}" if blower != 0
|
68
|
+
items << "mister" if mister
|
69
|
+
items << "aux=#{aux.inspect}"
|
70
|
+
|
71
|
+
result << items.join(' ') << ">"
|
72
|
+
end
|
15
73
|
end
|
16
74
|
end
|
17
75
|
end
|
@@ -5,7 +5,7 @@ module BWA
|
|
5
5
|
:filter2_enabled,
|
6
6
|
:filter2_hour, :filter2_minute, :filter2_duration_hours, :filter2_duration_minutes
|
7
7
|
|
8
|
-
MESSAGE_TYPE = "\
|
8
|
+
MESSAGE_TYPE = "\xbf\x23".force_encoding(Encoding::ASCII_8BIT)
|
9
9
|
MESSAGE_LENGTH = 8
|
10
10
|
|
11
11
|
def parse(data)
|
data/lib/bwa/messages/ready.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module BWA
|
2
2
|
module Messages
|
3
3
|
class SetTime < Message
|
4
|
-
MESSAGE_TYPE = "\
|
4
|
+
MESSAGE_TYPE = "\xbf\x21".force_encoding(Encoding::ASCII_8BIT)
|
5
5
|
MESSAGE_LENGTH = 2
|
6
6
|
|
7
7
|
attr_accessor :hour, :minute, :twenty_four_hour_time
|
data/lib/bwa/messages/status.rb
CHANGED
@@ -5,28 +5,37 @@ module BWA
|
|
5
5
|
:heating_mode,
|
6
6
|
:temperature_scale,
|
7
7
|
:twenty_four_hour_time,
|
8
|
+
:filter,
|
8
9
|
:heating,
|
9
10
|
:temperature_range,
|
10
11
|
:hour, :minute,
|
11
12
|
:circ_pump,
|
12
|
-
:
|
13
|
-
:
|
13
|
+
:blower,
|
14
|
+
:pumps,
|
15
|
+
:lights,
|
16
|
+
:mister,
|
17
|
+
:aux,
|
14
18
|
:current_temperature, :set_temperature
|
15
19
|
|
16
|
-
MESSAGE_TYPE = "\
|
17
|
-
|
20
|
+
MESSAGE_TYPE = "\xaf\x13".force_encoding(Encoding::ASCII_8BIT)
|
21
|
+
# additional features have been added in later versions
|
22
|
+
MESSAGE_LENGTH = 24..32
|
18
23
|
|
19
24
|
def initialize
|
25
|
+
@src = 0xff
|
20
26
|
self.priming = false
|
21
27
|
self.heating_mode = :ready
|
22
28
|
@temperature_scale = :fahrenheit
|
23
29
|
self.twenty_four_hour_time = false
|
30
|
+
self.filter = Array.new(2, false)
|
24
31
|
self.heating = false
|
25
32
|
self.temperature_range = :high
|
26
33
|
self.hour = self.minute = 0
|
27
34
|
self.circ_pump = false
|
28
|
-
self.
|
29
|
-
self.
|
35
|
+
self.pumps = Array.new(6, 0)
|
36
|
+
self.lights = Array.new(2, false)
|
37
|
+
self.mister = false
|
38
|
+
self.aux = Array.new(2, false)
|
30
39
|
self.set_temperature = 100
|
31
40
|
end
|
32
41
|
|
@@ -42,16 +51,30 @@ module BWA
|
|
42
51
|
flags = data[9].ord
|
43
52
|
self.temperature_scale = (flags & 0x01 == 0x01) ? :celsius : :fahrenheit
|
44
53
|
self.twenty_four_hour_time = (flags & 0x02 == 0x02)
|
54
|
+
filter[0] = (flags & 0x04 != 0)
|
55
|
+
filter[1] = (flags & 0x08 != 0)
|
45
56
|
flags = data[10].ord
|
46
57
|
self.heating = (flags & 0x30 != 0)
|
47
58
|
self.temperature_range = (flags & 0x04 == 0x04) ? :high : :low
|
48
59
|
flags = data[11].ord
|
49
|
-
|
50
|
-
|
60
|
+
pumps[0] = flags & 0x03
|
61
|
+
pumps[1] = (flags >> 2) & 0x03
|
62
|
+
pumps[2] = (flags >> 4) & 0x03
|
63
|
+
pumps[3] = (flags >> 6) & 0x03
|
64
|
+
flags = data[12].ord
|
65
|
+
pumps[4] = flags & 0x03
|
66
|
+
pumps[5] = (flags >> 2) & 0x03
|
67
|
+
|
51
68
|
flags = data[13].ord
|
52
69
|
self.circ_pump = (flags & 0x02 == 0x02)
|
70
|
+
self.blower = (flags & 0x0C == 0x0C)
|
53
71
|
flags = data[14].ord
|
54
|
-
|
72
|
+
lights[0] = (flags & 0x03 != 0)
|
73
|
+
lights[1] = ((flags >> 2) & 0x03 != 0)
|
74
|
+
flags = data[15].ord
|
75
|
+
self.mister = (flags & 0x01 == 0x01)
|
76
|
+
aux[0] = (flags & 0x08 != 0)
|
77
|
+
aux[1] = (flags & 0x10 != 0)
|
55
78
|
self.hour = data[3].ord
|
56
79
|
self.minute = data[4].ord
|
57
80
|
self.current_temperature = data[2].ord
|
@@ -134,16 +157,19 @@ module BWA
|
|
134
157
|
items << "priming" if priming
|
135
158
|
items << self.class.format_time(hour, minute, twenty_four_hour_time)
|
136
159
|
items << "#{current_temperature || '--'}/#{set_temperature}º#{temperature_scale.to_s[0].upcase}"
|
160
|
+
items << "filter=#{filter.inspect}"
|
137
161
|
items << heating_mode
|
138
162
|
items << "heating" if heating
|
139
163
|
items << temperature_range
|
140
164
|
items << "circ_pump" if circ_pump
|
141
|
-
items << "
|
142
|
-
items << "
|
143
|
-
items << "
|
165
|
+
items << "blower" if blower
|
166
|
+
items << "pumps=#{pumps.inspect}"
|
167
|
+
items << "lights=#{lights.inspect}"
|
168
|
+
items << "aux=#{aux.inspect}"
|
169
|
+
items << "mister" if mister
|
144
170
|
|
145
171
|
result << items.join(' ') << ">"
|
146
172
|
end
|
147
173
|
end
|
148
174
|
end
|
149
|
-
end
|
175
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module BWA
|
2
2
|
module Messages
|
3
3
|
class ToggleItem < Message
|
4
|
-
MESSAGE_TYPE = "\
|
4
|
+
MESSAGE_TYPE = "\xbf\x11".force_encoding(Encoding::ASCII_8BIT)
|
5
5
|
MESSAGE_LENGTH = 2
|
6
6
|
|
7
7
|
attr_accessor :item
|
@@ -15,8 +15,10 @@ module BWA
|
|
15
15
|
when 0x04; :pump1
|
16
16
|
when 0x05; :pump2
|
17
17
|
when 0x11; :light1
|
18
|
+
when 0x3c; :hold
|
18
19
|
when 0x50; :temperature_range
|
19
20
|
when 0x51; :heating_mode
|
21
|
+
else; data[0].ord
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
data/lib/bwa/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module BWA
|
2
|
-
VERSION = '1.
|
3
|
-
end
|
2
|
+
VERSION = '1.2.1'
|
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.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Cutrer
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: digest-crc
|
@@ -25,33 +25,47 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0.4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: mqtt
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.5.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.5.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: net-telnet-rfc2217
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.
|
47
|
+
version: 0.0.3
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.
|
54
|
+
version: 0.0.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: ccutrer-serialport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.0
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: byebug
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,7 +94,7 @@ dependencies:
|
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '13.0'
|
83
|
-
description:
|
97
|
+
description:
|
84
98
|
email: cody@cutrer.com'
|
85
99
|
executables:
|
86
100
|
- bwa_mqtt_bridge
|
@@ -114,7 +128,7 @@ homepage: https://github.com/ccutrer/bwa
|
|
114
128
|
licenses:
|
115
129
|
- MIT
|
116
130
|
metadata: {}
|
117
|
-
post_install_message:
|
131
|
+
post_install_message:
|
118
132
|
rdoc_options: []
|
119
133
|
require_paths:
|
120
134
|
- lib
|
@@ -129,8 +143,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
143
|
- !ruby/object:Gem::Version
|
130
144
|
version: '0'
|
131
145
|
requirements: []
|
132
|
-
rubygems_version: 3.
|
133
|
-
signing_key:
|
146
|
+
rubygems_version: 3.1.4
|
147
|
+
signing_key:
|
134
148
|
specification_version: 4
|
135
149
|
summary: Library for communication with Balboa Water Group's WiFi module or RS-485
|
136
150
|
test_files: []
|