balboa_worldwide_app 1.3.0 → 2.0.0
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 +121 -91
- data/lib/bwa/crc.rb +3 -1
- data/lib/bwa/discovery.rb +18 -17
- data/lib/bwa/logger.rb +9 -7
- data/lib/bwa/message.rb +67 -53
- data/lib/bwa/messages/configuration.rb +3 -1
- data/lib/bwa/messages/configuration_request.rb +3 -1
- data/lib/bwa/messages/control_configuration.rb +12 -9
- data/lib/bwa/messages/control_configuration_request.rb +13 -11
- data/lib/bwa/messages/filter_cycles.rb +50 -22
- data/lib/bwa/messages/ready.rb +3 -1
- data/lib/bwa/messages/{set_temperature.rb → set_target_temperature.rb} +5 -3
- data/lib/bwa/messages/set_temperature_scale.rb +5 -3
- data/lib/bwa/messages/set_time.rb +4 -2
- data/lib/bwa/messages/status.rb +51 -44
- data/lib/bwa/messages/toggle_item.rb +29 -27
- data/lib/bwa/proxy.rb +17 -18
- data/lib/bwa/server.rb +16 -14
- data/lib/bwa/version.rb +3 -1
- metadata +70 -24
- data/bin/bwa_client +0 -43
- data/bin/bwa_mqtt_bridge +0 -614
    
        data/lib/bwa/client.rb
    CHANGED
    
    | @@ -1,28 +1,59 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require  | 
| 4 | 
            -
            require  | 
| 3 | 
            +
            require "forwardable"
         | 
| 4 | 
            +
            require "uri"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require "bwa/logger"
         | 
| 7 | 
            +
            require "bwa/message"
         | 
| 5 8 |  | 
| 6 9 | 
             
            module BWA
         | 
| 7 10 | 
             
              class Client
         | 
| 8 | 
            -
                 | 
| 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
         | 
| 9 35 |  | 
| 10 36 | 
             
                def initialize(uri)
         | 
| 11 37 | 
             
                  uri = URI.parse(uri)
         | 
| 12 | 
            -
                   | 
| 13 | 
            -
             | 
| 38 | 
            +
                  case uri.scheme
         | 
| 39 | 
            +
                  when "tcp"
         | 
| 40 | 
            +
                    require "socket"
         | 
| 14 41 | 
             
                    @io = TCPSocket.new(uri.host, uri.port || 4257)
         | 
| 15 | 
            -
                   | 
| 16 | 
            -
                    require  | 
| 17 | 
            -
                    @io = Net::Telnet::RFC2217.new("Host" => uri.host, "Port" => uri.port || 23, "baud" =>  | 
| 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)
         | 
| 18 45 | 
             
                    @queue = []
         | 
| 19 46 | 
             
                  else
         | 
| 20 | 
            -
                    require  | 
| 21 | 
            -
                    @io = CCutrer::SerialPort.new(uri.path, baud:  | 
| 47 | 
            +
                    require "ccutrer-serialport"
         | 
| 48 | 
            +
                    @io = CCutrer::SerialPort.new(uri.path, baud: 115_200)
         | 
| 22 49 | 
             
                    @queue = []
         | 
| 23 50 | 
             
                  end
         | 
| 24 51 | 
             
                  @src = 0x0a
         | 
| 25 | 
            -
                  @buffer = ""
         | 
| 52 | 
            +
                  @buffer = +""
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def full_configuration?
         | 
| 56 | 
            +
                  status && control_configuration && configuration && filter_cycles
         | 
| 26 57 | 
             
                end
         | 
| 27 58 |  | 
| 28 59 | 
             
                def poll
         | 
| @@ -45,18 +76,20 @@ module BWA | |
| 45 76 | 
             
                  end
         | 
| 46 77 |  | 
| 47 78 | 
             
                  if message.is_a?(Messages::Ready) && (msg = @queue&.shift)
         | 
| 48 | 
            -
                     | 
| 79 | 
            +
                    unless BWA.verbosity < 1 && msg[3..4] == Messages::ControlConfigurationRequest::MESSAGE_TYPE
         | 
| 80 | 
            +
                      BWA.logger.debug "wrote: #{BWA.raw2str(msg)}"
         | 
| 81 | 
            +
                    end
         | 
| 49 82 | 
             
                    @io.write(msg)
         | 
| 50 83 | 
             
                  end
         | 
| 51 | 
            -
                  @ | 
| 52 | 
            -
                  @ | 
| 53 | 
            -
                  @ | 
| 54 | 
            -
                  @ | 
| 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)
         | 
| 55 88 | 
             
                  message
         | 
| 56 89 | 
             
                end
         | 
| 57 90 |  | 
| 58 91 | 
             
                def messages_pending?
         | 
| 59 | 
            -
                   | 
| 92 | 
            +
                  !!@io.wait_readable(0)
         | 
| 60 93 | 
             
                end
         | 
| 61 94 |  | 
| 62 95 | 
             
                def drain_message_queue
         | 
| @@ -65,12 +98,16 @@ module BWA | |
| 65 98 |  | 
| 66 99 | 
             
                def send_message(message)
         | 
| 67 100 | 
             
                  message.src = @src
         | 
| 68 | 
            -
                  BWA.logger.info "  to spa: #{message.inspect}" unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
         | 
| 69 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
         | 
| 70 105 | 
             
                  if @queue
         | 
| 71 106 | 
             
                    @queue.push(full_message)
         | 
| 72 107 | 
             
                  else
         | 
| 73 | 
            -
                     | 
| 108 | 
            +
                    unless BWA.verbosity < 1 && message.is_a?(Messages::ControlConfigurationRequest)
         | 
| 109 | 
            +
                      BWA.logger.debug "wrote: #{BWA.raw2str(full_message)}"
         | 
| 110 | 
            +
                    end
         | 
| 74 111 | 
             
                    @io.write(full_message)
         | 
| 75 112 | 
             
                  end
         | 
| 76 113 | 
             
                end
         | 
| @@ -95,12 +132,16 @@ module BWA | |
| 95 132 | 
             
                  send_message(Messages::ToggleItem.new(item))
         | 
| 96 133 | 
             
                end
         | 
| 97 134 |  | 
| 98 | 
            -
                def toggle_pump( | 
| 99 | 
            -
                  toggle_item( | 
| 135 | 
            +
                def toggle_pump(index)
         | 
| 136 | 
            +
                  toggle_item(index + 0x04)
         | 
| 100 137 | 
             
                end
         | 
| 101 138 |  | 
| 102 | 
            -
                def toggle_light( | 
| 103 | 
            -
                  toggle_item( | 
| 139 | 
            +
                def toggle_light(index)
         | 
| 140 | 
            +
                  toggle_item(index + 0x11)
         | 
| 141 | 
            +
                end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                def toggle_aux(index)
         | 
| 144 | 
            +
                  toggle_item(index + 0x16)
         | 
| 104 145 | 
             
                end
         | 
| 105 146 |  | 
| 106 147 | 
             
                def toggle_mister
         | 
| @@ -115,90 +156,77 @@ module BWA | |
| 115 156 | 
             
                  toggle_item(:hold)
         | 
| 116 157 | 
             
                end
         | 
| 117 158 |  | 
| 118 | 
            -
                def set_pump( | 
| 119 | 
            -
                  return unless  | 
| 120 | 
            -
             | 
| 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)
         | 
| 121 165 | 
             
                  times.times do
         | 
| 122 | 
            -
                    toggle_pump( | 
| 166 | 
            +
                    toggle_pump(index)
         | 
| 123 167 | 
             
                    sleep(0.1)
         | 
| 124 168 | 
             
                  end
         | 
| 125 169 | 
             
                end
         | 
| 126 170 |  | 
| 127 | 
            -
                % | 
| 171 | 
            +
                %i[light aux].each do |type|
         | 
| 172 | 
            +
                  suffix = "s" if type == :light
         | 
| 128 173 | 
             
                  class_eval <<-RUBY, __FILE__, __LINE__ + 1
         | 
| 129 | 
            -
                    def set_#{type}( | 
| 130 | 
            -
                      return unless  | 
| 131 | 
            -
                      return if  | 
| 132 | 
            -
             | 
| 174 | 
            +
                    def set_#{type}(index, desired)
         | 
| 175 | 
            +
                      return unless status
         | 
| 176 | 
            +
                      return if status.#{type}#{suffix}[index] == desired
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                      toggle_#{type}(index)
         | 
| 133 179 | 
             
                    end
         | 
| 134 180 | 
             
                  RUBY
         | 
| 135 181 | 
             
                end
         | 
| 136 182 |  | 
| 137 | 
            -
                def  | 
| 138 | 
            -
                  return unless  | 
| 139 | 
            -
                  return if  | 
| 183 | 
            +
                def mister=(desired)
         | 
| 184 | 
            +
                  return unless status
         | 
| 185 | 
            +
                  return if status.mister == desired
         | 
| 186 | 
            +
             | 
| 140 187 | 
             
                  toggle_mister
         | 
| 141 188 | 
             
                end
         | 
| 142 189 |  | 
| 143 | 
            -
                def  | 
| 144 | 
            -
                  return unless  | 
| 145 | 
            -
             | 
| 190 | 
            +
                def blower=(desired)
         | 
| 191 | 
            +
                  return unless status && configuration
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                  times = (desired - status.blower) % (configuration.blower + 1)
         | 
| 146 194 | 
             
                  times.times do
         | 
| 147 195 | 
             
                    toggle_blower
         | 
| 148 196 | 
             
                    sleep(0.1)
         | 
| 149 197 | 
             
                  end
         | 
| 150 198 | 
             
                end
         | 
| 151 199 |  | 
| 152 | 
            -
                def  | 
| 153 | 
            -
                  return unless  | 
| 154 | 
            -
                  return if  | 
| 200 | 
            +
                def hold=(desired)
         | 
| 201 | 
            +
                  return unless status
         | 
| 202 | 
            +
                  return if status.hold == desired
         | 
| 203 | 
            +
             | 
| 155 204 | 
             
                  toggle_hold
         | 
| 156 205 | 
             
                end
         | 
| 157 206 |  | 
| 158 207 | 
             
                # high range is 80-106 for F, 26-40 for C (by 0.5)
         | 
| 159 208 | 
             
                # low range is 50-99 for F, 10-26 for C (by 0.5)
         | 
| 160 | 
            -
                def  | 
| 161 | 
            -
                  return unless  | 
| 162 | 
            -
                  return if  | 
| 209 | 
            +
                def target_temperature=(desired)
         | 
| 210 | 
            +
                  return unless status
         | 
| 211 | 
            +
                  return if status.target_temperature == desired
         | 
| 163 212 |  | 
| 164 | 
            -
                  desired *= 2 if  | 
| 165 | 
            -
                  send_message(Messages:: | 
| 213 | 
            +
                  desired *= 2 if (status && status.temperature_scale == :celsius) || desired < 50
         | 
| 214 | 
            +
                  send_message(Messages::SetTargetTemperature.new(desired.round))
         | 
| 166 215 | 
             
                end
         | 
| 167 216 |  | 
| 168 | 
            -
                def set_time(hour, minute, twenty_four_hour_time  | 
| 217 | 
            +
                def set_time(hour, minute, twenty_four_hour_time: false)
         | 
| 169 218 | 
             
                  send_message(Messages::SetTime.new(hour, minute, twenty_four_hour_time))
         | 
| 170 219 | 
             
                end
         | 
| 171 220 |  | 
| 172 | 
            -
                def  | 
| 173 | 
            -
                  raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I | 
| 221 | 
            +
                def temperature_scale=(scale)
         | 
| 222 | 
            +
                  raise ArgumentError, "scale must be :fahrenheit or :celsius" unless %I[fahrenheit celsius].include?(scale)
         | 
| 223 | 
            +
             | 
| 174 224 | 
             
                  send_message(Messages::SetTemperatureScale.new(scale))
         | 
| 175 225 | 
             
                end
         | 
| 176 226 |  | 
| 177 | 
            -
                def  | 
| 178 | 
            -
                   | 
| 179 | 
            -
                   | 
| 180 | 
            -
                  if @last_filter_configuration
         | 
| 181 | 
            -
                    messagedata = if changedItem == "filter1hour" then changedValue.to_i.chr else @last_filter_configuration.filter1_hour.chr end
         | 
| 182 | 
            -
                    messagedata += if changedItem == "filter1minute" then changedValue.to_i.chr else @last_filter_configuration.filter1_minute.chr end
         | 
| 183 | 
            -
                    messagedata += if changedItem == "filter1durationhours" then changedValue.to_i.chr else @last_filter_configuration.filter1_duration_hours.chr end
         | 
| 184 | 
            -
                    messagedata += if changedItem == "filter1durationminutes" then changedValue.to_i.chr else @last_filter_configuration.filter1_duration_minutes.chr end
         | 
| 185 | 
            -
             | 
| 186 | 
            -
                    #The filter2 start hour is merged with the filter2 enable (who thought that was a good idea?) The high order bit of the byte is a flag
         | 
| 187 | 
            -
                    #to indicate this so we have to do a bit of different processing to do that
         | 
| 188 | 
            -
                    #Get the filter 2 start hour
         | 
| 189 | 
            -
                    starthour =  if changedItem == "filter2hour" then changedValue.to_i else @last_filter_configuration.filter2_hour end
         | 
| 190 | 
            -
                    #Check to see if we want filter 2 enabled (either because it changed or from the current configuration)
         | 
| 191 | 
            -
                    #If it is something that changed, we have to convert to boolean, if it is from the current config it already is a boolean
         | 
| 192 | 
            -
                    starthour |=  0x80 if (if changedItem == "filter2enabled" then (changedValue == "true" ? true : false) else @last_filter_configuration.filter2_enabled end)
         | 
| 193 | 
            -
             | 
| 194 | 
            -
                    messagedata += starthour.chr
         | 
| 195 | 
            -
             | 
| 196 | 
            -
                    messagedata += if changedItem == "filter2minute" then changedValue.to_i.chr else @last_filter_configuration.filter2_minute.chr end
         | 
| 197 | 
            -
                    messagedata += if changedItem == "filter2durationhours" then changedValue.to_i.chr else @last_filter_configuration.filter2_duration_hours.chr end
         | 
| 198 | 
            -
                    messagedata += if changedItem == "filter2durationminutes" then changedValue.to_i.chr else @last_filter_configuration.filter2_duration_minutes.chr end
         | 
| 199 | 
            -
             | 
| 200 | 
            -
                    send_message("\x0a\xbf\x23".force_encoding(Encoding::ASCII_8BIT) + messagedata)
         | 
| 201 | 
            -
                  end
         | 
| 227 | 
            +
                def update_filter_cycles(new_filter_cycles)
         | 
| 228 | 
            +
                  send_message(new_filter_cycles)
         | 
| 229 | 
            +
                  @filter_cycles = new_filter_cycles.dup
         | 
| 202 230 | 
             
                  request_filter_configuration
         | 
| 203 231 | 
             
                end
         | 
| 204 232 |  | 
| @@ -206,9 +234,10 @@ module BWA | |
| 206 234 | 
             
                  toggle_item(0x50)
         | 
| 207 235 | 
             
                end
         | 
| 208 236 |  | 
| 209 | 
            -
                def  | 
| 210 | 
            -
                  return unless  | 
| 211 | 
            -
                  return if  | 
| 237 | 
            +
                def temperature_range=(desired)
         | 
| 238 | 
            +
                  return unless status
         | 
| 239 | 
            +
                  return if status.temperature_range == desired
         | 
| 240 | 
            +
             | 
| 212 241 | 
             
                  toggle_temperature_range
         | 
| 213 242 | 
             
                end
         | 
| 214 243 |  | 
| @@ -216,19 +245,20 @@ module BWA | |
| 216 245 | 
             
                  toggle_item(:heating_mode)
         | 
| 217 246 | 
             
                end
         | 
| 218 247 |  | 
| 219 | 
            -
                HEATING_MODES = %I | 
| 220 | 
            -
                def  | 
| 221 | 
            -
                  raise ArgumentError, "heating_mode must be :ready or :rest" unless %I | 
| 222 | 
            -
                  return unless  | 
| 223 | 
            -
             | 
| 224 | 
            -
             | 
| 225 | 
            -
                              | 
| 226 | 
            -
             | 
| 227 | 
            -
             | 
| 228 | 
            -
             | 
| 229 | 
            -
             | 
| 230 | 
            -
             | 
| 231 | 
            -
             | 
| 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
         | 
| 232 262 | 
             
                  times.times { toggle_heating_mode }
         | 
| 233 263 | 
             
                end
         | 
| 234 264 | 
             
              end
         | 
    
        data/lib/bwa/crc.rb
    CHANGED
    
    
    
        data/lib/bwa/discovery.rb
    CHANGED
    
    | @@ -1,27 +1,27 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "socket"
         | 
| 4 | 
            +
            require "bwa/logger"
         | 
| 3 5 |  | 
| 4 6 | 
             
            module BWA
         | 
| 5 7 | 
             
              class Discovery
         | 
| 6 8 | 
             
                class << self
         | 
| 7 | 
            -
                  def discover(timeout = 5, exhaustive  | 
| 9 | 
            +
                  def discover(timeout = 5, exhaustive: false)
         | 
| 8 10 | 
             
                    socket = UDPSocket.new
         | 
| 9 11 | 
             
                    socket.bind("0.0.0.0", 0)
         | 
| 10 12 | 
             
                    socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
         | 
| 11 | 
            -
                    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"))
         | 
| 12 14 | 
             
                    spas = {}
         | 
| 13 15 | 
             
                    loop do
         | 
| 14 | 
            -
                       | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
                         | 
| 23 | 
            -
                      else
         | 
| 24 | 
            -
                        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
         | 
| 25 25 | 
             
                      end
         | 
| 26 26 | 
             
                    end
         | 
| 27 27 | 
             
                    spas
         | 
| @@ -29,11 +29,12 @@ module BWA | |
| 29 29 |  | 
| 30 30 | 
             
                  def advertise
         | 
| 31 31 | 
             
                    socket = UDPSocket.new
         | 
| 32 | 
            -
                    socket.bind("0.0.0.0",  | 
| 32 | 
            +
                    socket.bind("0.0.0.0", 30_303)
         | 
| 33 33 | 
             
                    msg = "BWGSPA\r\n00-15-27-00-00-01\r\n"
         | 
| 34 34 | 
             
                    loop do
         | 
| 35 35 | 
             
                      data, addr = socket.recvfrom(32)
         | 
| 36 | 
            -
                      next unless data ==  | 
| 36 | 
            +
                      next unless data == "Discovery: Who is out there?"
         | 
| 37 | 
            +
             | 
| 37 38 | 
             
                      ip = addr.last
         | 
| 38 39 | 
             
                      BWA.logger.info "Advertising to #{ip}"
         | 
| 39 40 | 
             
                      socket.sendmsg(msg, 0, Socket.sockaddr_in(addr[1], ip))
         | 
    
        data/lib/bwa/logger.rb
    CHANGED
    
    | @@ -1,4 +1,6 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "logger"
         | 
| 2 4 |  | 
| 3 5 | 
             
            module BWA
         | 
| 4 6 | 
             
              # This module logs to stdout by default, or you can provide a logger as BWA.logger.
         | 
| @@ -23,10 +25,10 @@ module BWA | |
| 23 25 | 
             
                attr_writer :logger, :verbosity
         | 
| 24 26 |  | 
| 25 27 | 
             
                def logger
         | 
| 26 | 
            -
                  @logger ||= Logger.new( | 
| 27 | 
            -
                     | 
| 28 | 
            -
                    log.level = ENV.fetch("LOG_LEVEL","WARN")
         | 
| 29 | 
            -
                    log.formatter = proc do |severity,  | 
| 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|
         | 
| 30 32 | 
             
                      "#{severity[0..0]}, #{msg2logstr(msg)}\n"
         | 
| 31 33 | 
             
                    end
         | 
| 32 34 | 
             
                  end
         | 
| @@ -42,14 +44,14 @@ module BWA | |
| 42 44 | 
             
                  when ::String
         | 
| 43 45 | 
             
                    msg
         | 
| 44 46 | 
             
                  when ::Exception
         | 
| 45 | 
            -
                    "#{ | 
| 47 | 
            +
                    "#{msg.message} (#{msg.class})\n#{msg.backtrace&.join("\n")}"
         | 
| 46 48 | 
             
                  else
         | 
| 47 49 | 
             
                    msg.inspect
         | 
| 48 50 | 
             
                  end
         | 
| 49 51 | 
             
                end
         | 
| 50 52 |  | 
| 51 53 | 
             
                def raw2str(data)
         | 
| 52 | 
            -
                  data. | 
| 54 | 
            +
                  data.unpack1("H*").gsub!(/(..)/, "\\1 ").chop!
         | 
| 53 55 | 
             
                end
         | 
| 54 56 | 
             
              end
         | 
| 55 57 | 
             
            end
         | 
    
        data/lib/bwa/message.rb
    CHANGED
    
    | @@ -1,5 +1,7 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "bwa/logger"
         | 
| 4 | 
            +
            require "bwa/crc"
         | 
| 3 5 |  | 
| 4 6 | 
             
            module BWA
         | 
| 5 7 | 
             
              class InvalidMessage < RuntimeError
         | 
| @@ -19,53 +21,58 @@ module BWA | |
| 19 21 |  | 
| 20 22 | 
             
                class << self
         | 
| 21 23 | 
             
                  def inherited(klass)
         | 
| 24 | 
            +
                    super
         | 
| 25 | 
            +
             | 
| 22 26 | 
             
                    @messages ||= []
         | 
| 23 27 | 
             
                    @messages << klass
         | 
| 24 28 | 
             
                  end
         | 
| 25 29 |  | 
| 26 30 | 
             
                  # Ignore (parse and throw away) messages of these types.
         | 
| 27 31 | 
             
                  IGNORED_MESSAGES = [
         | 
| 28 | 
            -
                    "\xbf\x00".force_encoding(Encoding::ASCII_8BIT), | 
| 29 | 
            -
                    "\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
         | 
| 30 | 
            -
                    "\xbf\x07".force_encoding(Encoding::ASCII_8BIT) | 
| 31 | 
            -
                  ]
         | 
| 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
         | 
| 32 36 |  | 
| 33 37 | 
             
                  # Don't log messages of these types, even in DEBUG mode.
         | 
| 34 38 | 
             
                  # They are very frequent and would swamp the logs.
         | 
| 35 39 | 
             
                  def common_messages
         | 
| 36 | 
            -
                    @ | 
| 40 | 
            +
                    @common_messages ||= begin
         | 
| 37 41 | 
             
                      msgs = []
         | 
| 38 | 
            -
                       | 
| 39 | 
            -
                         | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
                         | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 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
         | 
| 48 56 | 
             
                      msgs
         | 
| 49 57 | 
             
                    end
         | 
| 50 | 
            -
                    @COMMON_MESSAGES
         | 
| 51 58 | 
             
                  end
         | 
| 52 59 |  | 
| 53 60 | 
             
                  def parse(data)
         | 
| 54 61 | 
             
                    offset = -1
         | 
| 55 | 
            -
                    message_type = length =  | 
| 62 | 
            +
                    message_type = length = nil
         | 
| 56 63 | 
             
                    loop do
         | 
| 57 64 | 
             
                      offset += 1
         | 
| 58 65 | 
             
                      # Not enough data for a full message; return and hope for more
         | 
| 59 66 | 
             
                      return nil if data.length - offset < 5
         | 
| 60 67 |  | 
| 61 68 | 
             
                      # Keep scanning until message start char
         | 
| 62 | 
            -
                      next unless data[offset] ==  | 
| 69 | 
            +
                      next unless data[offset] == "~"
         | 
| 63 70 |  | 
| 64 71 | 
             
                      # Read length (safe since we have at least 5 chars)
         | 
| 65 72 | 
             
                      length = data[offset + 1].ord
         | 
| 66 73 |  | 
| 67 74 | 
             
                      # No message is this short or this long; keep scanning
         | 
| 68 | 
            -
                      next if length < 5  | 
| 75 | 
            +
                      next if (length < 5) || (length >= "~".ord)
         | 
| 69 76 |  | 
| 70 77 | 
             
                      # don't have enough data for what this message wants;
         | 
| 71 78 | 
             
                      # return and hope for more (yes this might cause a
         | 
| @@ -74,7 +81,7 @@ module BWA | |
| 74 81 | 
             
                      return nil if length + 2 > data.length - offset
         | 
| 75 82 |  | 
| 76 83 | 
             
                      # Not properly terminated; keep scanning
         | 
| 77 | 
            -
                      next unless data[offset + length + 1] ==  | 
| 84 | 
            +
                      next unless data[offset + length + 1] == "~"
         | 
| 78 85 |  | 
| 79 86 | 
             
                      # Not a valid checksum; keep scanning
         | 
| 80 87 | 
             
                      next unless CRC.checksum(data.slice(offset + 1, length - 1)) == data[offset + length].ord
         | 
| @@ -84,8 +91,11 @@ module BWA | |
| 84 91 | 
             
                    end
         | 
| 85 92 |  | 
| 86 93 | 
             
                    message_type = data.slice(offset + 3, 2)
         | 
| 87 | 
            -
                    BWA.logger.debug "discarding invalid data prior to message #{BWA.raw2str(data[0...offset])}" unless offset | 
| 88 | 
            -
                     | 
| 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
         | 
| 89 99 |  | 
| 90 100 | 
             
                    src = data[offset + 2].ord
         | 
| 91 101 | 
             
                    klass = @messages.find { |k| k::MESSAGE_TYPE == message_type }
         | 
| @@ -95,13 +105,18 @@ module BWA | |
| 95 105 |  | 
| 96 106 | 
             
                    if klass
         | 
| 97 107 | 
             
                      valid_length = if klass::MESSAGE_LENGTH.respond_to?(:include?)
         | 
| 98 | 
            -
             | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 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)
         | 
| 101 115 | 
             
                      end
         | 
| 102 | 
            -
                      raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}", data) unless valid_length
         | 
| 103 116 | 
             
                    else
         | 
| 104 | 
            -
                      BWA.logger.info | 
| 117 | 
            +
                      BWA.logger.info(
         | 
| 118 | 
            +
                        "Unrecognized message type #{BWA.raw2str(message_type)}: #{BWA.raw2str(data.slice(offset, length + 2))}"
         | 
| 119 | 
            +
                      )
         | 
| 105 120 | 
             
                      klass = Unrecognized
         | 
| 106 121 | 
             
                    end
         | 
| 107 122 |  | 
| @@ -113,53 +128,52 @@ module BWA | |
| 113 128 | 
             
                    [message, offset + length + 2]
         | 
| 114 129 | 
             
                  end
         | 
| 115 130 |  | 
| 116 | 
            -
                  def format_time(hour, minute, twenty_four_hour_time  | 
| 131 | 
            +
                  def format_time(hour, minute, twenty_four_hour_time: true)
         | 
| 117 132 | 
             
                    if twenty_four_hour_time
         | 
| 118 | 
            -
                       | 
| 133 | 
            +
                      format("%02d:%02d", hour, minute)
         | 
| 119 134 | 
             
                    else
         | 
| 120 135 | 
             
                      print_hour = hour % 12
         | 
| 121 | 
            -
                      print_hour = 12 if print_hour | 
| 122 | 
            -
                       | 
| 136 | 
            +
                      print_hour = 12 if print_hour.zero?
         | 
| 137 | 
            +
                      format("%d:%02d%s", print_hour, minute, hour >= 12 ? "PM" : "AM")
         | 
| 123 138 | 
             
                    end
         | 
| 124 | 
            -
                    "#{print_hour}:#{"%02d" % minute}#{am_pm}"
         | 
| 125 139 | 
             
                  end
         | 
| 126 140 |  | 
| 127 | 
            -
                  def format_duration( | 
| 128 | 
            -
                    " | 
| 141 | 
            +
                  def format_duration(minutes)
         | 
| 142 | 
            +
                    format("%d:%02d", minutes / 60, minutes % 60)
         | 
| 129 143 | 
             
                  end
         | 
| 130 144 | 
             
                end
         | 
| 131 145 |  | 
| 132 | 
            -
                attr_reader :raw_data | 
| 146 | 
            +
                attr_reader :raw_data
         | 
| 133 147 |  | 
| 134 148 | 
             
                def initialize
         | 
| 135 149 | 
             
                  # most messages we're sending come from this address
         | 
| 136 150 | 
             
                  @src = 0x0a
         | 
| 137 151 | 
             
                end
         | 
| 138 152 |  | 
| 139 | 
            -
                def parse(_data)
         | 
| 140 | 
            -
                end
         | 
| 153 | 
            +
                def parse(_data); end
         | 
| 141 154 |  | 
| 142 155 | 
             
                def serialize(message = "")
         | 
| 143 156 | 
             
                  length = message.length + 5
         | 
| 144 | 
            -
                  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)
         | 
| 145 159 | 
             
                  checksum = CRC.checksum(full_message)
         | 
| 146 | 
            -
                  "\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT)
         | 
| 160 | 
            +
                  (+"\x7e#{full_message}#{checksum.chr}\x7e").force_encoding(Encoding::ASCII_8BIT)
         | 
| 147 161 | 
             
                end
         | 
| 148 162 |  | 
| 149 163 | 
             
                def inspect
         | 
| 150 | 
            -
                  "#<#{self.class.name} #{raw_data. | 
| 164 | 
            +
                  "#<#{self.class.name} #{raw_data.unpack1("H*")}>"
         | 
| 151 165 | 
             
                end
         | 
| 152 166 | 
             
              end
         | 
| 153 167 | 
             
            end
         | 
| 154 168 |  | 
| 155 | 
            -
            require  | 
| 156 | 
            -
            require  | 
| 157 | 
            -
            require  | 
| 158 | 
            -
            require  | 
| 159 | 
            -
            require  | 
| 160 | 
            -
            require  | 
| 161 | 
            -
            require  | 
| 162 | 
            -
            require  | 
| 163 | 
            -
            require  | 
| 164 | 
            -
            require  | 
| 165 | 
            -
            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"
         | 
| @@ -1,7 +1,9 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module BWA
         | 
| 2 4 | 
             
              module Messages
         | 
| 3 5 | 
             
                class Configuration < Message
         | 
| 4 | 
            -
                  MESSAGE_TYPE = "\xbf\x94".force_encoding(Encoding::ASCII_8BIT)
         | 
| 6 | 
            +
                  MESSAGE_TYPE = (+"\xbf\x94").force_encoding(Encoding::ASCII_8BIT)
         | 
| 5 7 | 
             
                  MESSAGE_LENGTH = 25
         | 
| 6 8 |  | 
| 7 9 | 
             
                  def inspect
         | 
| @@ -1,7 +1,9 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module BWA
         | 
| 2 4 | 
             
              module Messages
         | 
| 3 5 | 
             
                class ConfigurationRequest < Message
         | 
| 4 | 
            -
                  MESSAGE_TYPE = "\xbf\x04".force_encoding(Encoding::ASCII_8BIT)
         | 
| 6 | 
            +
                  MESSAGE_TYPE = (+"\xbf\x04").force_encoding(Encoding::ASCII_8BIT)
         | 
| 5 7 | 
             
                  MESSAGE_LENGTH = 0
         | 
| 6 8 |  | 
| 7 9 | 
             
                  def inspect
         |