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 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: []