balboa_worldwide_app 1.2.5 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/exe/bwa_client +41 -0
- data/exe/bwa_mqtt_bridge +394 -0
- data/{bin → exe}/bwa_proxy +3 -2
- data/{bin → exe}/bwa_server +3 -2
- data/lib/balboa_worldwide_app.rb +3 -1
- data/lib/bwa/client.rb +152 -79
- data/lib/bwa/crc.rb +3 -1
- data/lib/bwa/discovery.rb +19 -17
- data/lib/bwa/logger.rb +57 -0
- data/lib/bwa/message.rb +85 -41
- data/lib/bwa/messages/configuration.rb +7 -1
- data/lib/bwa/messages/configuration_request.rb +3 -1
- data/lib/bwa/messages/control_configuration.rb +13 -9
- data/lib/bwa/messages/control_configuration_request.rb +22 -2
- data/lib/bwa/messages/filter_cycles.rb +50 -22
- data/lib/bwa/messages/ready.rb +7 -1
- data/lib/bwa/messages/{set_temperature.rb → set_target_temperature.rb} +6 -3
- data/lib/bwa/messages/set_temperature_scale.rb +6 -3
- data/lib/bwa/messages/set_time.rb +5 -2
- data/lib/bwa/messages/status.rb +61 -47
- data/lib/bwa/messages/toggle_item.rb +30 -18
- data/lib/bwa/proxy.rb +19 -19
- data/lib/bwa/server.rb +22 -19
- data/lib/bwa/version.rb +3 -1
- metadata +77 -24
- data/bin/bwa_client +0 -43
- data/bin/bwa_mqtt_bridge +0 -283
data/lib/bwa/client.rb
CHANGED
@@ -1,26 +1,59 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "forwardable"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
require "bwa/logger"
|
7
|
+
require "bwa/message"
|
4
8
|
|
5
9
|
module BWA
|
6
10
|
class Client
|
7
|
-
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :status, :control_configuration, :configuration, :filter_cycles
|
14
|
+
|
15
|
+
delegate model: :control_configuration
|
16
|
+
delegate %i[hold
|
17
|
+
hold?
|
18
|
+
priming
|
19
|
+
priming?
|
20
|
+
heating_mode
|
21
|
+
temperature_scale
|
22
|
+
twenty_four_hour_time
|
23
|
+
twenty_four_hour_time?
|
24
|
+
heating
|
25
|
+
heating?
|
26
|
+
temperature_range
|
27
|
+
current_temperature
|
28
|
+
target_temperature
|
29
|
+
circulation_pump
|
30
|
+
blower
|
31
|
+
mister
|
32
|
+
pumps
|
33
|
+
lights
|
34
|
+
aux] => :status
|
8
35
|
|
9
36
|
def initialize(uri)
|
10
37
|
uri = URI.parse(uri)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
38
|
+
case uri.scheme
|
39
|
+
when "tcp"
|
40
|
+
require "socket"
|
41
|
+
@io = TCPSocket.new(uri.host, uri.port || 4257)
|
42
|
+
when "telnet", "rfc2217"
|
43
|
+
require "net/telnet/rfc2217"
|
44
|
+
@io = Net::Telnet::RFC2217.new("Host" => uri.host, "Port" => uri.port || 23, "baud" => 115_200)
|
17
45
|
@queue = []
|
18
46
|
else
|
19
|
-
require
|
20
|
-
@io = CCutrer::SerialPort.new(uri.path, baud:
|
47
|
+
require "ccutrer-serialport"
|
48
|
+
@io = CCutrer::SerialPort.new(uri.path, baud: 115_200)
|
21
49
|
@queue = []
|
22
50
|
end
|
23
|
-
@
|
51
|
+
@src = 0x0a
|
52
|
+
@buffer = +""
|
53
|
+
end
|
54
|
+
|
55
|
+
def full_configuration?
|
56
|
+
status && control_configuration && configuration && filter_cycles
|
24
57
|
end
|
25
58
|
|
26
59
|
def poll
|
@@ -43,18 +76,20 @@ module BWA
|
|
43
76
|
end
|
44
77
|
|
45
78
|
if message.is_a?(Messages::Ready) && (msg = @queue&.shift)
|
46
|
-
|
79
|
+
unless BWA.verbosity < 1 && msg[3..4] == Messages::ControlConfigurationRequest::MESSAGE_TYPE
|
80
|
+
BWA.logger.debug "wrote: #{BWA.raw2str(msg)}"
|
81
|
+
end
|
47
82
|
@io.write(msg)
|
48
83
|
end
|
49
|
-
@
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
84
|
+
@status = message.dup if message.is_a?(Messages::Status)
|
85
|
+
@filter_cycles = message.dup if message.is_a?(Messages::FilterCycles)
|
86
|
+
@control_configuration = message.dup if message.is_a?(Messages::ControlConfiguration)
|
87
|
+
@configuration = message.dup if message.is_a?(Messages::ControlConfiguration2)
|
53
88
|
message
|
54
89
|
end
|
55
90
|
|
56
91
|
def messages_pending?
|
57
|
-
|
92
|
+
!!@io.wait_readable(0)
|
58
93
|
end
|
59
94
|
|
60
95
|
def drain_message_queue
|
@@ -62,132 +97,170 @@ module BWA
|
|
62
97
|
end
|
63
98
|
|
64
99
|
def send_message(message)
|
65
|
-
|
66
|
-
full_message =
|
67
|
-
|
68
|
-
|
100
|
+
message.src = @src
|
101
|
+
full_message = message.serialize
|
102
|
+
unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
|
103
|
+
BWA.logger.info " to spa: #{message.inspect}"
|
104
|
+
end
|
69
105
|
if @queue
|
70
106
|
@queue.push(full_message)
|
71
107
|
else
|
108
|
+
unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
|
109
|
+
BWA.logger.debug "wrote: #{BWA.raw2str(full_message)}"
|
110
|
+
end
|
72
111
|
@io.write(full_message)
|
73
112
|
end
|
74
113
|
end
|
75
114
|
|
76
115
|
def request_configuration
|
77
|
-
send_message(
|
116
|
+
send_message(Messages::ConfigurationRequest.new)
|
78
117
|
end
|
79
118
|
|
80
119
|
def request_control_info2
|
81
|
-
send_message(
|
120
|
+
send_message(Messages::ControlConfigurationRequest.new(2))
|
82
121
|
end
|
83
122
|
|
84
123
|
def request_control_info
|
85
|
-
send_message(
|
124
|
+
send_message(Messages::ControlConfigurationRequest.new(1))
|
86
125
|
end
|
87
126
|
|
88
127
|
def request_filter_configuration
|
89
|
-
send_message(
|
128
|
+
send_message(Messages::ControlConfigurationRequest.new(3))
|
90
129
|
end
|
91
130
|
|
92
131
|
def toggle_item(item)
|
93
|
-
send_message(
|
132
|
+
send_message(Messages::ToggleItem.new(item))
|
133
|
+
end
|
134
|
+
|
135
|
+
def toggle_pump(index)
|
136
|
+
toggle_item(index + 0x04)
|
94
137
|
end
|
95
138
|
|
96
|
-
def
|
97
|
-
toggle_item(
|
139
|
+
def toggle_light(index)
|
140
|
+
toggle_item(index + 0x11)
|
98
141
|
end
|
99
142
|
|
100
|
-
def
|
101
|
-
toggle_item(
|
143
|
+
def toggle_aux(index)
|
144
|
+
toggle_item(index + 0x16)
|
102
145
|
end
|
103
146
|
|
104
147
|
def toggle_mister
|
105
|
-
toggle_item(
|
148
|
+
toggle_item(:mister)
|
106
149
|
end
|
107
150
|
|
108
151
|
def toggle_blower
|
109
|
-
toggle_item(
|
152
|
+
toggle_item(:blower)
|
110
153
|
end
|
111
154
|
|
112
|
-
def
|
113
|
-
|
114
|
-
|
155
|
+
def toggle_hold
|
156
|
+
toggle_item(:hold)
|
157
|
+
end
|
158
|
+
|
159
|
+
def set_pump(index, desired)
|
160
|
+
return unless status && configuration
|
161
|
+
|
162
|
+
desired = 0 if desired == false
|
163
|
+
desired = 1 if desired == true
|
164
|
+
times = (desired - status.pumps[index]) % (configuration.pumps[index] + 1)
|
115
165
|
times.times do
|
116
|
-
toggle_pump(
|
166
|
+
toggle_pump(index)
|
117
167
|
sleep(0.1)
|
118
168
|
end
|
119
169
|
end
|
120
170
|
|
121
|
-
%
|
171
|
+
%i[light aux].each do |type|
|
172
|
+
suffix = "s" if type == :light
|
122
173
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
123
|
-
def set_#{type}(
|
124
|
-
return unless
|
125
|
-
return if
|
126
|
-
|
174
|
+
def set_#{type}(index, desired)
|
175
|
+
return unless status
|
176
|
+
return if status.#{type}#{suffix}[index] == desired
|
177
|
+
|
178
|
+
toggle_#{type}(index)
|
127
179
|
end
|
128
180
|
RUBY
|
129
181
|
end
|
130
182
|
|
131
|
-
def
|
132
|
-
return unless
|
133
|
-
return if
|
183
|
+
def mister=(desired)
|
184
|
+
return unless status
|
185
|
+
return if status.mister == desired
|
186
|
+
|
134
187
|
toggle_mister
|
135
188
|
end
|
136
189
|
|
137
|
-
def
|
138
|
-
return unless
|
139
|
-
|
190
|
+
def blower=(desired)
|
191
|
+
return unless status && configuration
|
192
|
+
|
193
|
+
desired = 0 if desired == false
|
194
|
+
desired = 1 if desired == true
|
195
|
+
times = (desired - status.blower) % (configuration.blower + 1)
|
140
196
|
times.times do
|
141
197
|
toggle_blower
|
142
198
|
sleep(0.1)
|
143
199
|
end
|
144
200
|
end
|
145
201
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
202
|
+
def hold=(desired)
|
203
|
+
return unless status
|
204
|
+
return if status.hold == desired
|
205
|
+
|
206
|
+
toggle_hold
|
207
|
+
end
|
208
|
+
|
209
|
+
# high range is 80-106 for F, 26-40 for C (by 0.5)
|
210
|
+
# low range is 50-99 for F, 10-26 for C (by 0.5)
|
211
|
+
def target_temperature=(desired)
|
212
|
+
return unless status
|
213
|
+
return if status.target_temperature == desired
|
214
|
+
|
215
|
+
desired *= 2 if (status && status.temperature_scale == :celsius) || desired < 50
|
216
|
+
send_message(Messages::SetTargetTemperature.new(desired.round))
|
217
|
+
end
|
218
|
+
|
219
|
+
def set_time(hour, minute, twenty_four_hour_time: false)
|
220
|
+
send_message(Messages::SetTime.new(hour, minute, twenty_four_hour_time))
|
151
221
|
end
|
152
222
|
|
153
|
-
def
|
154
|
-
|
155
|
-
|
223
|
+
def temperature_scale=(scale)
|
224
|
+
raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I[fahrenheit celsius].include?(scale)
|
225
|
+
|
226
|
+
send_message(Messages::SetTemperatureScale.new(scale))
|
156
227
|
end
|
157
228
|
|
158
|
-
def
|
159
|
-
|
160
|
-
|
161
|
-
|
229
|
+
def update_filter_cycles(new_filter_cycles)
|
230
|
+
send_message(new_filter_cycles)
|
231
|
+
@filter_cycles = new_filter_cycles.dup
|
232
|
+
request_filter_configuration
|
162
233
|
end
|
163
234
|
|
164
235
|
def toggle_temperature_range
|
165
236
|
toggle_item(0x50)
|
166
237
|
end
|
167
238
|
|
168
|
-
def
|
169
|
-
return unless
|
170
|
-
return if
|
239
|
+
def temperature_range=(desired)
|
240
|
+
return unless status
|
241
|
+
return if status.temperature_range == desired
|
242
|
+
|
171
243
|
toggle_temperature_range
|
172
244
|
end
|
173
245
|
|
174
246
|
def toggle_heating_mode
|
175
|
-
toggle_item(
|
176
|
-
end
|
177
|
-
|
178
|
-
HEATING_MODES = %I
|
179
|
-
def
|
180
|
-
raise ArgumentError, "heating_mode must be :ready or :rest" unless %I
|
181
|
-
return unless
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
247
|
+
toggle_item(:heating_mode)
|
248
|
+
end
|
249
|
+
|
250
|
+
HEATING_MODES = %I[ready rest ready_in_rest].freeze
|
251
|
+
def heating_mode=(desired)
|
252
|
+
raise ArgumentError, "heating_mode must be :ready or :rest" unless %I[ready rest].include?(desired)
|
253
|
+
return unless status
|
254
|
+
|
255
|
+
times = if (status.heating_mode == :ready && desired == :rest) ||
|
256
|
+
(status.heating_mode == :rest && desired == :ready) ||
|
257
|
+
(status.heating_mode == :ready_in_rest && desired == :rest)
|
258
|
+
1
|
259
|
+
elsif status.heating_mode == :ready_in_rest && desired == :ready
|
260
|
+
2
|
261
|
+
else
|
262
|
+
0
|
263
|
+
end
|
191
264
|
times.times { toggle_heating_mode }
|
192
265
|
end
|
193
266
|
end
|
data/lib/bwa/crc.rb
CHANGED
data/lib/bwa/discovery.rb
CHANGED
@@ -1,26 +1,27 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require "bwa/logger"
|
2
5
|
|
3
6
|
module BWA
|
4
7
|
class Discovery
|
5
8
|
class << self
|
6
|
-
def discover(timeout = 5, exhaustive
|
9
|
+
def discover(timeout = 5, exhaustive: false)
|
7
10
|
socket = UDPSocket.new
|
8
11
|
socket.bind("0.0.0.0", 0)
|
9
12
|
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
10
|
-
socket.sendmsg("Discovery: Who is out there?", 0, Socket.sockaddr_in(
|
13
|
+
socket.sendmsg("Discovery: Who is out there?", 0, Socket.sockaddr_in(30_303, "255.255.255.255"))
|
11
14
|
spas = {}
|
12
15
|
loop do
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
else
|
23
|
-
break
|
16
|
+
break unless socket.wait_readable(timeout)
|
17
|
+
|
18
|
+
msg, ip = socket.recvfrom(64)
|
19
|
+
ip = ip[2]
|
20
|
+
name, mac = msg.split("\r\n")
|
21
|
+
name.strip!
|
22
|
+
if mac.start_with?("00-15-27-")
|
23
|
+
spas[ip] = name
|
24
|
+
break unless exhaustive
|
24
25
|
end
|
25
26
|
end
|
26
27
|
spas
|
@@ -28,13 +29,14 @@ module BWA
|
|
28
29
|
|
29
30
|
def advertise
|
30
31
|
socket = UDPSocket.new
|
31
|
-
socket.bind("0.0.0.0",
|
32
|
+
socket.bind("0.0.0.0", 30_303)
|
32
33
|
msg = "BWGSPA\r\n00-15-27-00-00-01\r\n"
|
33
34
|
loop do
|
34
35
|
data, addr = socket.recvfrom(32)
|
35
|
-
next unless data ==
|
36
|
+
next unless data == "Discovery: Who is out there?"
|
37
|
+
|
36
38
|
ip = addr.last
|
37
|
-
|
39
|
+
BWA.logger.info "Advertising to #{ip}"
|
38
40
|
socket.sendmsg(msg, 0, Socket.sockaddr_in(addr[1], ip))
|
39
41
|
end
|
40
42
|
end
|
data/lib/bwa/logger.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module BWA
|
6
|
+
# This module logs to stdout by default, or you can provide a logger as BWA.logger.
|
7
|
+
# If using default logger, set LOG_LEVEL in the environment to control logging.
|
8
|
+
#
|
9
|
+
# Log levels are:
|
10
|
+
#
|
11
|
+
# FATAL - fatal errors
|
12
|
+
# ERROR - handled errors
|
13
|
+
# WARN - problems while parsing known messages
|
14
|
+
# INFO - unrecognized messages
|
15
|
+
# DEBUG - all messages
|
16
|
+
#
|
17
|
+
# Certain very frequent messages are suppressed by default even in DEBUG mode.
|
18
|
+
# Set LOG_VERBOSITY to one of the following levels to see these:
|
19
|
+
#
|
20
|
+
# 0 - default
|
21
|
+
# 1 - show status messages
|
22
|
+
# 2 - show ready and nothing-to-send messages
|
23
|
+
#
|
24
|
+
class << self
|
25
|
+
attr_writer :logger, :verbosity
|
26
|
+
|
27
|
+
def logger
|
28
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
29
|
+
$stdout.sync = true
|
30
|
+
log.level = ENV.fetch("LOG_LEVEL", "WARN")
|
31
|
+
log.formatter = proc do |severity, _datetime, _progname, msg|
|
32
|
+
"#{severity[0..0]}, #{msg2logstr(msg)}\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def verbosity
|
38
|
+
@verbosity ||= ENV.fetch("LOG_VERBOSITY", "0").to_i
|
39
|
+
@verbosity
|
40
|
+
end
|
41
|
+
|
42
|
+
def msg2logstr(msg)
|
43
|
+
case msg
|
44
|
+
when ::String
|
45
|
+
msg
|
46
|
+
when ::Exception
|
47
|
+
"#{msg.message} (#{msg.class})\n#{msg.backtrace&.join("\n")}"
|
48
|
+
else
|
49
|
+
msg.inspect
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def raw2str(data)
|
54
|
+
data.unpack1("H*").gsub!(/(..)/, "\\1 ").chop!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/bwa/message.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bwa/logger"
|
4
|
+
require "bwa/crc"
|
2
5
|
|
3
6
|
module BWA
|
4
7
|
class InvalidMessage < RuntimeError
|
@@ -11,30 +14,65 @@ module BWA
|
|
11
14
|
end
|
12
15
|
|
13
16
|
class Message
|
17
|
+
attr_accessor :src
|
18
|
+
|
14
19
|
class Unrecognized < Message
|
15
20
|
end
|
16
21
|
|
17
22
|
class << self
|
18
23
|
def inherited(klass)
|
24
|
+
super
|
25
|
+
|
19
26
|
@messages ||= []
|
20
27
|
@messages << klass
|
21
28
|
end
|
22
29
|
|
30
|
+
# Ignore (parse and throw away) messages of these types.
|
31
|
+
IGNORED_MESSAGES = [
|
32
|
+
(+"\xbf\x00").force_encoding(Encoding::ASCII_8BIT), # request for new clients
|
33
|
+
(+"\xbf\xe1").force_encoding(Encoding::ASCII_8BIT),
|
34
|
+
(+"\xbf\x07").force_encoding(Encoding::ASCII_8BIT) # nothing to send
|
35
|
+
].freeze
|
36
|
+
|
37
|
+
# Don't log messages of these types, even in DEBUG mode.
|
38
|
+
# They are very frequent and would swamp the logs.
|
39
|
+
def common_messages
|
40
|
+
@common_messages ||= begin
|
41
|
+
msgs = []
|
42
|
+
unless BWA.verbosity >= 1
|
43
|
+
msgs += [
|
44
|
+
Messages::Status::MESSAGE_TYPE,
|
45
|
+
(+"\xbf\xe1").force_encoding(Encoding::ASCII_8BIT)
|
46
|
+
]
|
47
|
+
end
|
48
|
+
unless BWA.verbosity >= 2
|
49
|
+
msgs += [
|
50
|
+
(+"\xbf\x00").force_encoding(Encoding::ASCII_8BIT),
|
51
|
+
(+"\xbf\xe1").force_encoding(Encoding::ASCII_8BIT),
|
52
|
+
Messages::Ready::MESSAGE_TYPE,
|
53
|
+
(+"\xbf\x07").force_encoding(Encoding::ASCII_8BIT)
|
54
|
+
]
|
55
|
+
end
|
56
|
+
msgs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
23
60
|
def parse(data)
|
24
61
|
offset = -1
|
25
|
-
message_type = length =
|
62
|
+
message_type = length = nil
|
26
63
|
loop do
|
27
64
|
offset += 1
|
65
|
+
# Not enough data for a full message; return and hope for more
|
28
66
|
return nil if data.length - offset < 5
|
29
67
|
|
30
68
|
# Keep scanning until message start char
|
31
|
-
next unless data[offset] ==
|
69
|
+
next unless data[offset] == "~"
|
32
70
|
|
33
71
|
# Read length (safe since we have at least 5 chars)
|
34
72
|
length = data[offset + 1].ord
|
35
73
|
|
36
74
|
# No message is this short or this long; keep scanning
|
37
|
-
next if length < 5
|
75
|
+
next if (length < 5) || (length >= "~".ord)
|
38
76
|
|
39
77
|
# don't have enough data for what this message wants;
|
40
78
|
# return and hope for more (yes this might cause a
|
@@ -43,7 +81,7 @@ module BWA
|
|
43
81
|
return nil if length + 2 > data.length - offset
|
44
82
|
|
45
83
|
# Not properly terminated; keep scanning
|
46
|
-
next unless data[offset + length + 1] ==
|
84
|
+
next unless data[offset + length + 1] == "~"
|
47
85
|
|
48
86
|
# Not a valid checksum; keep scanning
|
49
87
|
next unless CRC.checksum(data.slice(offset + 1, length - 1)) == data[offset + length].ord
|
@@ -52,27 +90,33 @@ module BWA
|
|
52
90
|
break
|
53
91
|
end
|
54
92
|
|
55
|
-
|
56
|
-
|
93
|
+
message_type = data.slice(offset + 3, 2)
|
94
|
+
BWA.logger.debug "discarding invalid data prior to message #{BWA.raw2str(data[0...offset])}" unless offset.zero?
|
95
|
+
unless common_messages.include?(message_type)
|
96
|
+
BWA.logger.debug " read: #{BWA.raw2str(data.slice(offset,
|
97
|
+
length + 2))}"
|
98
|
+
end
|
57
99
|
|
58
100
|
src = data[offset + 2].ord
|
59
|
-
message_type = data.slice(offset + 3, 2)
|
60
101
|
klass = @messages.find { |k| k::MESSAGE_TYPE == message_type }
|
61
102
|
|
62
|
-
|
63
|
-
return [nil, offset + length + 2] if
|
64
|
-
"\xbf\x00".force_encoding(Encoding::ASCII_8BIT),
|
65
|
-
"\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
|
66
|
-
"\xbf\x07".force_encoding(Encoding::ASCII_8BIT)].include?(message_type)
|
103
|
+
# Ignore these message types
|
104
|
+
return [nil, offset + length + 2] if IGNORED_MESSAGES.include?(message_type)
|
67
105
|
|
68
106
|
if klass
|
69
107
|
valid_length = if klass::MESSAGE_LENGTH.respond_to?(:include?)
|
70
|
-
|
71
|
-
|
72
|
-
|
108
|
+
klass::MESSAGE_LENGTH.include?(length - 5)
|
109
|
+
else
|
110
|
+
length - 5 == klass::MESSAGE_LENGTH
|
111
|
+
end
|
112
|
+
unless valid_length
|
113
|
+
raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}",
|
114
|
+
data)
|
73
115
|
end
|
74
|
-
raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}", data) unless valid_length
|
75
116
|
else
|
117
|
+
BWA.logger.info(
|
118
|
+
"Unrecognized message type #{BWA.raw2str(message_type)}: #{BWA.raw2str(data.slice(offset, length + 2))}"
|
119
|
+
)
|
76
120
|
klass = Unrecognized
|
77
121
|
end
|
78
122
|
|
@@ -80,56 +124,56 @@ module BWA
|
|
80
124
|
message.parse(data.slice(offset + 5, length - 5))
|
81
125
|
message.instance_variable_set(:@raw_data, data.slice(offset, length + 2))
|
82
126
|
message.instance_variable_set(:@src, src)
|
127
|
+
BWA.logger.debug "from spa: #{message.inspect}" unless common_messages.include?(message_type)
|
83
128
|
[message, offset + length + 2]
|
84
129
|
end
|
85
130
|
|
86
|
-
def format_time(hour, minute, twenty_four_hour_time
|
131
|
+
def format_time(hour, minute, twenty_four_hour_time: true)
|
87
132
|
if twenty_four_hour_time
|
88
|
-
|
133
|
+
format("%02d:%02d", hour, minute)
|
89
134
|
else
|
90
135
|
print_hour = hour % 12
|
91
|
-
print_hour = 12 if print_hour
|
92
|
-
|
136
|
+
print_hour = 12 if print_hour.zero?
|
137
|
+
format("%d:%02d%s", print_hour, minute, hour >= 12 ? "PM" : "AM")
|
93
138
|
end
|
94
|
-
"#{print_hour}:#{"%02d" % minute}#{am_pm}"
|
95
139
|
end
|
96
140
|
|
97
|
-
def format_duration(
|
98
|
-
"
|
141
|
+
def format_duration(minutes)
|
142
|
+
format("%d:%02d", minutes / 60, minutes % 60)
|
99
143
|
end
|
100
144
|
end
|
101
145
|
|
102
|
-
attr_reader :raw_data
|
146
|
+
attr_reader :raw_data
|
103
147
|
|
104
148
|
def initialize
|
105
149
|
# most messages we're sending come from this address
|
106
150
|
@src = 0x0a
|
107
151
|
end
|
108
152
|
|
109
|
-
def parse(_data)
|
110
|
-
end
|
153
|
+
def parse(_data); end
|
111
154
|
|
112
155
|
def serialize(message = "")
|
113
156
|
length = message.length + 5
|
114
|
-
full_message = "#{length.chr}#{src.chr}#{self.class::MESSAGE_TYPE}#{message}"
|
157
|
+
full_message = (+"#{length.chr}#{src.chr}#{self.class::MESSAGE_TYPE}#{message}")
|
158
|
+
.force_encoding(Encoding::ASCII_8BIT)
|
115
159
|
checksum = CRC.checksum(full_message)
|
116
|
-
"\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT)
|
160
|
+
(+"\x7e#{full_message}#{checksum.chr}\x7e").force_encoding(Encoding::ASCII_8BIT)
|
117
161
|
end
|
118
162
|
|
119
163
|
def inspect
|
120
|
-
"#<#{self.class.name} #{raw_data.
|
164
|
+
"#<#{self.class.name} #{raw_data.unpack1("H*")}>"
|
121
165
|
end
|
122
166
|
end
|
123
167
|
end
|
124
168
|
|
125
|
-
require
|
126
|
-
require
|
127
|
-
require
|
128
|
-
require
|
129
|
-
require
|
130
|
-
require
|
131
|
-
require
|
132
|
-
require
|
133
|
-
require
|
134
|
-
require
|
135
|
-
require
|
169
|
+
require "bwa/messages/configuration"
|
170
|
+
require "bwa/messages/configuration_request"
|
171
|
+
require "bwa/messages/control_configuration"
|
172
|
+
require "bwa/messages/control_configuration_request"
|
173
|
+
require "bwa/messages/filter_cycles"
|
174
|
+
require "bwa/messages/ready"
|
175
|
+
require "bwa/messages/set_target_temperature"
|
176
|
+
require "bwa/messages/set_temperature_scale"
|
177
|
+
require "bwa/messages/set_time"
|
178
|
+
require "bwa/messages/status"
|
179
|
+
require "bwa/messages/toggle_item"
|