balboa_worldwide_app 1.2.4 → 2.0.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/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 +151 -78
- 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 +78 -25
- data/bin/bwa_client +0 -43
- data/bin/bwa_mqtt_bridge +0 -282
data/lib/bwa/client.rb
CHANGED
@@ -1,24 +1,59 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
require "bwa/logger"
|
7
|
+
require "bwa/message"
|
2
8
|
|
3
9
|
module BWA
|
4
10
|
class Client
|
5
|
-
|
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
|
6
35
|
|
7
36
|
def initialize(uri)
|
8
37
|
uri = URI.parse(uri)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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)
|
15
45
|
@queue = []
|
16
46
|
else
|
17
|
-
require
|
18
|
-
@io = CCutrer::SerialPort.new(uri.path, baud:
|
47
|
+
require "ccutrer-serialport"
|
48
|
+
@io = CCutrer::SerialPort.new(uri.path, baud: 115_200)
|
19
49
|
@queue = []
|
20
50
|
end
|
21
|
-
@
|
51
|
+
@src = 0x0a
|
52
|
+
@buffer = +""
|
53
|
+
end
|
54
|
+
|
55
|
+
def full_configuration?
|
56
|
+
status && control_configuration && configuration && filter_cycles
|
22
57
|
end
|
23
58
|
|
24
59
|
def poll
|
@@ -41,18 +76,20 @@ module BWA
|
|
41
76
|
end
|
42
77
|
|
43
78
|
if message.is_a?(Messages::Ready) && (msg = @queue&.shift)
|
44
|
-
|
79
|
+
unless BWA.verbosity < 1 && msg[3..4] == Messages::ControlConfigurationRequest::MESSAGE_TYPE
|
80
|
+
BWA.logger.debug "wrote: #{BWA.raw2str(msg)}"
|
81
|
+
end
|
45
82
|
@io.write(msg)
|
46
83
|
end
|
47
|
-
@
|
48
|
-
@
|
49
|
-
@
|
50
|
-
@
|
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)
|
51
88
|
message
|
52
89
|
end
|
53
90
|
|
54
91
|
def messages_pending?
|
55
|
-
|
92
|
+
!!@io.wait_readable(0)
|
56
93
|
end
|
57
94
|
|
58
95
|
def drain_message_queue
|
@@ -60,132 +97,168 @@ module BWA
|
|
60
97
|
end
|
61
98
|
|
62
99
|
def send_message(message)
|
63
|
-
|
64
|
-
full_message =
|
65
|
-
|
66
|
-
|
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
|
67
105
|
if @queue
|
68
106
|
@queue.push(full_message)
|
69
107
|
else
|
108
|
+
unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
|
109
|
+
BWA.logger.debug "wrote: #{BWA.raw2str(full_message)}"
|
110
|
+
end
|
70
111
|
@io.write(full_message)
|
71
112
|
end
|
72
113
|
end
|
73
114
|
|
74
115
|
def request_configuration
|
75
|
-
send_message(
|
116
|
+
send_message(Messages::ConfigurationRequest.new)
|
76
117
|
end
|
77
118
|
|
78
119
|
def request_control_info2
|
79
|
-
send_message(
|
120
|
+
send_message(Messages::ControlConfigurationRequest.new(2))
|
80
121
|
end
|
81
122
|
|
82
123
|
def request_control_info
|
83
|
-
send_message(
|
124
|
+
send_message(Messages::ControlConfigurationRequest.new(1))
|
84
125
|
end
|
85
126
|
|
86
127
|
def request_filter_configuration
|
87
|
-
send_message(
|
128
|
+
send_message(Messages::ControlConfigurationRequest.new(3))
|
88
129
|
end
|
89
130
|
|
90
131
|
def toggle_item(item)
|
91
|
-
send_message(
|
132
|
+
send_message(Messages::ToggleItem.new(item))
|
133
|
+
end
|
134
|
+
|
135
|
+
def toggle_pump(index)
|
136
|
+
toggle_item(index + 0x04)
|
92
137
|
end
|
93
138
|
|
94
|
-
def
|
95
|
-
toggle_item(
|
139
|
+
def toggle_light(index)
|
140
|
+
toggle_item(index + 0x11)
|
96
141
|
end
|
97
142
|
|
98
|
-
def
|
99
|
-
toggle_item(
|
143
|
+
def toggle_aux(index)
|
144
|
+
toggle_item(index + 0x16)
|
100
145
|
end
|
101
146
|
|
102
147
|
def toggle_mister
|
103
|
-
toggle_item(
|
148
|
+
toggle_item(:mister)
|
104
149
|
end
|
105
150
|
|
106
151
|
def toggle_blower
|
107
|
-
toggle_item(
|
152
|
+
toggle_item(:blower)
|
108
153
|
end
|
109
154
|
|
110
|
-
def
|
111
|
-
|
112
|
-
|
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)
|
113
165
|
times.times do
|
114
|
-
toggle_pump(
|
166
|
+
toggle_pump(index)
|
115
167
|
sleep(0.1)
|
116
168
|
end
|
117
169
|
end
|
118
170
|
|
119
|
-
%
|
171
|
+
%i[light aux].each do |type|
|
172
|
+
suffix = "s" if type == :light
|
120
173
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
121
|
-
def set_#{type}(
|
122
|
-
return unless
|
123
|
-
return if
|
124
|
-
|
174
|
+
def set_#{type}(index, desired)
|
175
|
+
return unless status
|
176
|
+
return if status.#{type}#{suffix}[index] == desired
|
177
|
+
|
178
|
+
toggle_#{type}(index)
|
125
179
|
end
|
126
180
|
RUBY
|
127
181
|
end
|
128
182
|
|
129
|
-
def
|
130
|
-
return unless
|
131
|
-
return if
|
183
|
+
def mister=(desired)
|
184
|
+
return unless status
|
185
|
+
return if status.mister == desired
|
186
|
+
|
132
187
|
toggle_mister
|
133
188
|
end
|
134
189
|
|
135
|
-
def
|
136
|
-
return unless
|
137
|
-
|
190
|
+
def blower=(desired)
|
191
|
+
return unless status && configuration
|
192
|
+
|
193
|
+
times = (desired - status.blower) % (configuration.blower + 1)
|
138
194
|
times.times do
|
139
195
|
toggle_blower
|
140
196
|
sleep(0.1)
|
141
197
|
end
|
142
198
|
end
|
143
199
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
200
|
+
def hold=(desired)
|
201
|
+
return unless status
|
202
|
+
return if status.hold == desired
|
203
|
+
|
204
|
+
toggle_hold
|
205
|
+
end
|
206
|
+
|
207
|
+
# high range is 80-106 for F, 26-40 for C (by 0.5)
|
208
|
+
# low range is 50-99 for F, 10-26 for C (by 0.5)
|
209
|
+
def target_temperature=(desired)
|
210
|
+
return unless status
|
211
|
+
return if status.target_temperature == desired
|
212
|
+
|
213
|
+
desired *= 2 if (status && status.temperature_scale == :celsius) || desired < 50
|
214
|
+
send_message(Messages::SetTargetTemperature.new(desired.round))
|
215
|
+
end
|
216
|
+
|
217
|
+
def set_time(hour, minute, twenty_four_hour_time: false)
|
218
|
+
send_message(Messages::SetTime.new(hour, minute, twenty_four_hour_time))
|
149
219
|
end
|
150
220
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
221
|
+
def temperature_scale=(scale)
|
222
|
+
raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I[fahrenheit celsius].include?(scale)
|
223
|
+
|
224
|
+
send_message(Messages::SetTemperatureScale.new(scale))
|
154
225
|
end
|
155
226
|
|
156
|
-
def
|
157
|
-
|
158
|
-
|
159
|
-
|
227
|
+
def update_filter_cycles(new_filter_cycles)
|
228
|
+
send_message(new_filter_cycles)
|
229
|
+
@filter_cycles = new_filter_cycles.dup
|
230
|
+
request_filter_configuration
|
160
231
|
end
|
161
232
|
|
162
233
|
def toggle_temperature_range
|
163
234
|
toggle_item(0x50)
|
164
235
|
end
|
165
236
|
|
166
|
-
def
|
167
|
-
return unless
|
168
|
-
return if
|
237
|
+
def temperature_range=(desired)
|
238
|
+
return unless status
|
239
|
+
return if status.temperature_range == desired
|
240
|
+
|
169
241
|
toggle_temperature_range
|
170
242
|
end
|
171
243
|
|
172
244
|
def toggle_heating_mode
|
173
|
-
toggle_item(
|
174
|
-
end
|
175
|
-
|
176
|
-
HEATING_MODES = %I
|
177
|
-
def
|
178
|
-
raise ArgumentError, "heating_mode must be :ready or :rest" unless %I
|
179
|
-
return unless
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
245
|
+
toggle_item(:heating_mode)
|
246
|
+
end
|
247
|
+
|
248
|
+
HEATING_MODES = %I[ready rest ready_in_rest].freeze
|
249
|
+
def heating_mode=(desired)
|
250
|
+
raise ArgumentError, "heating_mode must be :ready or :rest" unless %I[ready rest].include?(desired)
|
251
|
+
return unless status
|
252
|
+
|
253
|
+
times = if (status.heating_mode == :ready && desired == :rest) ||
|
254
|
+
(status.heating_mode == :rest && desired == :ready) ||
|
255
|
+
(status.heating_mode == :ready_in_rest && desired == :rest)
|
256
|
+
1
|
257
|
+
elsif status.heating_mode == :ready_in_rest && desired == :ready
|
258
|
+
2
|
259
|
+
else
|
260
|
+
0
|
261
|
+
end
|
189
262
|
times.times { toggle_heating_mode }
|
190
263
|
end
|
191
264
|
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"
|