balboa_worldwide_app 1.2.5 → 2.0.2

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.
data/lib/bwa/client.rb CHANGED
@@ -1,26 +1,59 @@
1
- require 'uri'
1
+ # frozen_string_literal: true
2
2
 
3
- require 'bwa/message'
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
- attr_reader :last_status, :last_control_configuration, :last_control_configuration2, :last_filter_configuration
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
- if uri.scheme == 'tcp'
12
- require 'socket'
13
- @io = TCPSocket.new(uri.host, uri.port || 4217)
14
- elsif uri.scheme == 'telnet' || uri.scheme == 'rfc2217'
15
- require 'net/telnet/rfc2217'
16
- @io = Net::Telnet::RFC2217.new("Host" => uri.host, "Port" => uri.port || 23, "baud" => 115200)
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 'ccutrer-serialport'
20
- @io = CCutrer::SerialPort.new(uri.path, baud: 115200)
47
+ require "ccutrer-serialport"
48
+ @io = CCutrer::SerialPort.new(uri.path, baud: 115_200)
21
49
  @queue = []
22
50
  end
23
- @buffer = ""
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
- puts "wrote #{msg.unpack('H*').first}"
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
- @last_status = message.dup if message.is_a?(Messages::Status)
50
- @last_filter_configuration = message.dup if message.is_a?(Messages::FilterCycles)
51
- @last_control_configuration = message.dup if message.is_a?(Messages::ControlConfiguration)
52
- @last_control_configuration2 = message.dup if message.is_a?(Messages::ControlConfiguration2)
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
- !!IO.select([@io], nil, nil, 0)
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
- length = message.length + 2
66
- full_message = "#{length.chr}#{message}".force_encoding(Encoding::ASCII_8BIT)
67
- checksum = CRC.checksum(full_message)
68
- full_message = "\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT)
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("\x0a\xbf\x04")
116
+ send_message(Messages::ConfigurationRequest.new)
78
117
  end
79
118
 
80
119
  def request_control_info2
81
- send_message("\x0a\xbf\x22\x00\x00\x01")
120
+ send_message(Messages::ControlConfigurationRequest.new(2))
82
121
  end
83
122
 
84
123
  def request_control_info
85
- send_message("\x0a\xbf\x22\x02\x00\x00")
124
+ send_message(Messages::ControlConfigurationRequest.new(1))
86
125
  end
87
126
 
88
127
  def request_filter_configuration
89
- send_message("\x0a\xbf\x22\x01\x00\x00")
128
+ send_message(Messages::ControlConfigurationRequest.new(3))
90
129
  end
91
130
 
92
131
  def toggle_item(item)
93
- send_message("\x0a\xbf\x11#{item.chr}\x00")
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 toggle_pump(i)
97
- toggle_item(i + 3)
139
+ def toggle_light(index)
140
+ toggle_item(index + 0x11)
98
141
  end
99
142
 
100
- def toggle_light(i)
101
- toggle_item(i + 0x10)
143
+ def toggle_aux(index)
144
+ toggle_item(index + 0x16)
102
145
  end
103
146
 
104
147
  def toggle_mister
105
- toggle_item(0x0e)
148
+ toggle_item(:mister)
106
149
  end
107
150
 
108
151
  def toggle_blower
109
- toggle_item(0x0c)
152
+ toggle_item(:blower)
110
153
  end
111
154
 
112
- def set_pump(i, desired)
113
- return unless last_status && last_control_configuration2
114
- times = (desired - last_status.pumps[i - 1]) % (last_control_configuration2.pumps[i - 1] + 1)
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(i)
166
+ toggle_pump(index)
117
167
  sleep(0.1)
118
168
  end
119
169
  end
120
170
 
121
- %w{light aux}.each do |type|
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}(i, desired)
124
- return unless last_status
125
- return if last_status.#{type}s[i - 1] == desired
126
- toggle_#{type}(i)
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 set_mister(desired)
132
- return unless last_status
133
- return if last_status.mister == desired
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 set_blower(desired)
138
- return unless last_status && last_control_configuration2
139
- times = (desired - last_status.blower) % (last_control_configuration2.blower + 1)
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
- # high range is 80-104 for F, 26-40 for C (by 0.5)
147
- # low range is 50-80 for F, 10-26 for C (by 0.5)
148
- def set_temperature(desired)
149
- desired *= 2 if last_status && last_status.temperature_scale == :celsius || desired < 50
150
- send_message("\x0a\xbf\x20#{desired.round.chr}")
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 set_time(hour, minute, twenty_four_hour_time = false)
154
- hour |= 0x80 if twenty_four_hour_time
155
- send_message("\x0a\xbf\x21".force_encoding(Encoding::ASCII_8BIT) + hour.chr + minute.chr)
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 set_temperature_scale(scale)
159
- raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I{fahrenheit :celsius}.include?(scale)
160
- arg = scale == :fahrenheit ? 0 : 1
161
- send_message("\x0a\xbf\x27\x01".force_encoding(Encoding::ASCII_8BIT) + arg.chr)
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 set_temperature_range(desired)
169
- return unless last_status
170
- return if last_status.temperature_range == desired
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(0x51)
176
- end
177
-
178
- HEATING_MODES = %I{ready rest ready_in_rest}.freeze
179
- def set_heating_mode(desired)
180
- raise ArgumentError, "heating_mode must be :ready or :rest" unless %I{ready rest}.include?(desired)
181
- return unless last_status
182
- times = if last_status.heating_mode == :ready && desired == :rest ||
183
- last_status.heating_mode == :rest && desired == :ready ||
184
- last_status.heating_mode == :ready_in_rest && desired == :rest
185
- 1
186
- elsif last_status.heating_mode == :ready_in_rest && desired == :ready
187
- 2
188
- else
189
- 0
190
- end
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
@@ -1,4 +1,6 @@
1
- require 'digest/crc'
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/crc"
2
4
 
3
5
  module BWA
4
6
  class CRC < Digest::CRC8
data/lib/bwa/discovery.rb CHANGED
@@ -1,26 +1,27 @@
1
- require 'socket'
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 = false)
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(30303, '255.255.255.255'))
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
- if IO.select([socket], nil, nil, timeout)
14
- msg, ip = socket.recvfrom(64)
15
- ip = ip[2]
16
- name, mac = msg.split("\r\n")
17
- name.strip!
18
- if mac.start_with?("00-15-27-")
19
- spas[ip] = name
20
- break unless exhaustive
21
- end
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", 30303)
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 == 'Discovery: Who is out there?'
36
+ next unless data == "Discovery: Who is out there?"
37
+
36
38
  ip = addr.last
37
- puts "Advertising to #{ip}"
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
- require 'bwa/crc'
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 = message_class = nil
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 or length >= '~'.ord
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
- puts "discarding invalid data prior to message #{data[0...offset].unpack('H*').first}" unless offset == 0
56
- #puts "read #{data.slice(offset, length + 2).unpack('H*').first}"
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
- klass::MESSAGE_LENGTH.include?(length - 5)
71
- else
72
- length - 5 == klass::MESSAGE_LENGTH
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 = true)
131
+ def format_time(hour, minute, twenty_four_hour_time: true)
87
132
  if twenty_four_hour_time
88
- print_hour = "%02d" % hour
133
+ format("%02d:%02d", hour, minute)
89
134
  else
90
135
  print_hour = hour % 12
91
- print_hour = 12 if print_hour == 0
92
- am_pm = (hour >= 12 ? "PM" : "AM")
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(hours, minutes)
98
- "#{hours}:#{"%02d" % minutes}"
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, :src
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}".force_encoding(Encoding::ASCII_8BIT)
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.unpack("H*").first}>"
164
+ "#<#{self.class.name} #{raw_data.unpack1("H*")}>"
121
165
  end
122
166
  end
123
167
  end
124
168
 
125
- require 'bwa/messages/configuration'
126
- require 'bwa/messages/configuration_request'
127
- require 'bwa/messages/control_configuration'
128
- require 'bwa/messages/control_configuration_request'
129
- require 'bwa/messages/filter_cycles'
130
- require 'bwa/messages/ready'
131
- require 'bwa/messages/set_temperature'
132
- require 'bwa/messages/set_temperature_scale'
133
- require 'bwa/messages/set_time'
134
- require 'bwa/messages/status'
135
- require 'bwa/messages/toggle_item'
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"