balboa_worldwide_app 1.0.0 → 1.2.1

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: 7daff60d63c09a23f5628f69102fa722745c343d5c3641d7d87f09e47be7549a
4
- data.tar.gz: b70859344d26e0f7a54b3a8ffea6389637683c5b5f31de5a0defb8bb52176c0a
3
+ metadata.gz: 537f4b0048b2f972b06f90fc404fc9589845cc7550e5305a19e0bab892e647e4
4
+ data.tar.gz: 79fd85b3e8e95b6fade48c8135c170a52956b60d69199a84a31bed4ccd6f7c61
5
5
  SHA512:
6
- metadata.gz: '085e33a524d9410cbd069592804a716448031eece734bf5d2209318ef387ef9b2cda6fe941f4956681015889ca70f6f4429805eabfbc56e0bd23322262c3f850'
7
- data.tar.gz: 749b99bc33e6df19dec5f276206a62556310be025daec5b8c71f061f8fd45510d7a6f58f201655f8b9cfc0bfaf826cf7c7b5b3d5b43755bbedc44ed87bdd70b5
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
- if message.hour != now.hour || message.minute != now.min
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 unless %w{ready rest ready_in_rest}.include?(value)
63
- # @bwa.set_heating_mode(value.to_sym)
64
- when "temperaturescale"
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
- # @bwa.set_temperature_scale(value.to_sym)
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
- # @bwa.set_temperature_range(value.to_sym)
74
- when %r{^spa/(pump[12])/set}
75
- @bwa.send(:"set_#{$1}", value.to_i)
76
- when "spa/light1/set"
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.set_light1(value == 'true')
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
- publish("spa/$properties", "priming,heatingmode,temperaturescale,24htime,heating,temperaturerange,circpump,pump1,pump2,light1,currenttemperature,settemperature")
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("$state", "ready")
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(host, port = 4257)
8
- if host =~ %r{^/dev}
9
- require 'serialport'
10
- @io = SerialPort.open(host, "baud" => 115200)
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 'socket'
14
- @io = TCPSocket.new(host, port)
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
- while message.nil?
21
- begin
22
- message = Message.parse(@io)
23
- if message.is_a?(Messages::Ready) && (msg = @queue.shift)
24
- @io.write(msg)
25
- end
26
- rescue BWA::InvalidMessage => e
27
- unless e.message =~ /Incorrect data length/
28
- puts e.message
29
- puts e.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
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(args)
71
- send_message("\x0a\xbf\x11#{args}")
90
+ def toggle_item(item)
91
+ send_message("\x0a\xbf\x11#{item.chr}\x00")
72
92
  end
73
93
 
74
- def toggle_light1
75
- toggle_item("\x11\x00")
94
+ def toggle_pump(i)
95
+ toggle_item(i + 3)
76
96
  end
77
97
 
78
- def toggle_pump1
79
- toggle_item("\x04\x00")
98
+ def toggle_light(i)
99
+ toggle_item(i + 0x10)
80
100
  end
81
101
 
82
- def toggle_pump2
83
- toggle_item("\x05\x00")
102
+ def toggle_mister
103
+ toggle_item(0x0e)
84
104
  end
85
105
 
86
- (1..2).each do |i|
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
- def set_pump#{i}(desired)
89
- return unless last_status
90
- times = (desired - last_status.pump#{i}) % 3
91
- times.times do
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 set_light1(desired)
129
+ def set_mister(desired)
100
130
  return unless last_status
101
- return if last_status.light1 == desired
102
- toggle_light1
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(io)
21
- io = StringIO.new(io) if io.is_a?(String)
22
- data = ''
23
- # skim through until a start-of-message indicator
24
- until data[0] == '~'
25
- data = io.read(1)
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
- data.concat(io.read(1))
29
- length = data[-1].ord
30
+ next unless data[offset] == '~'
31
+ length = data[offset + 1].ord
32
+ # impossible message
33
+ next if length < 5
30
34
 
31
- if length < 5
32
- bytes = data.bytes
33
- bytes.shift
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
- data.concat(io.read(length))
39
- if data.length != length + 2
40
- data.bytes.reverse.each { |b| io.ungetbyte(b) }
41
- raise InvalidMessage.new("Incorrect data length (received #{data.length - 2}, expected #{length})", data)
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
- message_type = data[2..4]
45
- klass = @messages.find { |k| k::MESSAGE_TYPE == message_type }
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
- unless data[-1] == '~'
48
- bytes = data.bytes
49
- bytes.shift
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
- "\xfe\xbf\x00".force_encoding(Encoding::ASCII_8BIT),
63
- "\x10\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
64
- "\x10\xbf\x07".force_encoding(Encoding::ASCII_8BIT)].include?(message_type)
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
- raise InvalidMessage.new("Unrecognized message #{message_type.unpack("H*").first}", data) unless klass
67
- raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}", data) unless length - 5 == klass::MESSAGE_LENGTH
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[5..-2])
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,7 +1,7 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class Configuration < Message
4
- MESSAGE_TYPE = "\x0a\xbf\x94".force_encoding(Encoding::ASCII_8BIT)
4
+ MESSAGE_TYPE = "\xbf\x94".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 25
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class ConfigurationRequest < Message
4
- MESSAGE_TYPE = "\x0a\xbf\x04".force_encoding(Encoding::ASCII_8BIT)
4
+ MESSAGE_TYPE = "\xbf\x04".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 0
6
6
 
7
7
  def inspect
@@ -1,17 +1,75 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class ControlConfiguration < Message
4
- MESSAGE_TYPE = "\x0a\xbf\x24".force_encoding(Encoding::ASCII_8BIT)
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 = "\x0a\xbf\x2e".force_encoding(Encoding::ASCII_8BIT)
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
@@ -1,7 +1,7 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class ControlConfigurationRequest < Message
4
- MESSAGE_TYPE = "\x0a\xbf\x22".force_encoding(Encoding::ASCII_8BIT)
4
+ MESSAGE_TYPE = "\xbf\x22".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 3
6
6
 
7
7
  attr_accessor :type
@@ -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 = "\x0a\xbf\x23".force_encoding(Encoding::ASCII_8BIT)
8
+ MESSAGE_TYPE = "\xbf\x23".force_encoding(Encoding::ASCII_8BIT)
9
9
  MESSAGE_LENGTH = 8
10
10
 
11
11
  def parse(data)
@@ -1,7 +1,7 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class Ready < Message
4
- MESSAGE_TYPE = "\x10\xbf\06".force_encoding(Encoding::ASCII_8BIT)
4
+ MESSAGE_TYPE = "\xbf\06".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 0
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class SetTemperature < Message
4
- MESSAGE_TYPE = "\x0a\xbf\x20".force_encoding(Encoding::ASCII_8BIT)
4
+ MESSAGE_TYPE = "\xbf\x20".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 1
6
6
 
7
7
  attr_accessor :temperature
@@ -1,7 +1,7 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class SetTemperatureScale < Message
4
- MESSAGE_TYPE = "\x0a\xbf\x27".force_encoding(Encoding::ASCII_8BIT)
4
+ MESSAGE_TYPE = "\xbf\x27".force_encoding(Encoding::ASCII_8BIT)
5
5
  MESSAGE_LENGTH = 2
6
6
 
7
7
  attr_accessor :scale
@@ -1,7 +1,7 @@
1
1
  module BWA
2
2
  module Messages
3
3
  class SetTime < Message
4
- MESSAGE_TYPE = "\x0a\xbf\x21".force_encoding(Encoding::ASCII_8BIT)
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
@@ -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
- :pump1, :pump2,
13
- :light1,
13
+ :blower,
14
+ :pumps,
15
+ :lights,
16
+ :mister,
17
+ :aux,
14
18
  :current_temperature, :set_temperature
15
19
 
16
- MESSAGE_TYPE = "\xff\xaf\x13".force_encoding(Encoding::ASCII_8BIT)
17
- MESSAGE_LENGTH = 24
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.pump1 = self.pump2 = 0
29
- self.light1 = false
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
- self.pump1 = flags & 0x03
50
- self.pump2 = (flags / 4) & 0x03
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
- self.light1 = (flags & 0x03 == 0x03)
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 << "pump1=#{pump1}" unless pump1 == 0
142
- items << "pump2=#{pump2}" unless pump2 == 0
143
- items << "light1" if light1
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 = "\x0a\xbf\x11".force_encoding(Encoding::ASCII_8BIT)
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.0.0'
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.0.0
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: 2020-05-12 00:00:00.000000000 Z
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: serialport
28
+ name: mqtt
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 1.3.1
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: 1.3.1
40
+ version: 0.5.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: mqtt
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.5.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.5.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.0.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: []