class Object

Public Instance Methods

check_config_ok?(config, configfile) click to toggle source

Checks Hash with config-data

# File lib/hodmin/hodmin_tools.rb, line 391
def check_config_ok?(config, configfile)
  status_ok = true
  if config['mqtt']['host'] == 'mqtt.example.com'
    puts "ERR: No valid config-file found.\nPlease edit config file: #{configfile}."
    status_ok = false
  end

  if !config['mqtt']['base_topic'].empty? && config['mqtt']['base_topic'].split(//).last != '/'
    puts "ERR: mqtt: base_topic MUST end with '/'. Base_topic given: #{config['mqtt']['base_topic']}"
    status_ok = false
  end
  status_ok
end
cleanup_instance_var_name(inputstring) click to toggle source

Helper to remove some special chars from string to avoid problems in instance_variable_set:

# File lib/hodmin/hodmin_tools.rb, line 430
def cleanup_instance_var_name(inputstring)
        # translate some signs into char-representation:
        str = inputstring
        to_be_replaced = [['%','PCT'],[':','CLN'],['?','QMARK'],['&','AMPS'],['!','EXCLM'],['.','DOT']]
        to_be_replaced.each{|org,rpl| str = str.gsub(org,'_' + rpl + '_')}
        # translate some signs into '_':
        str.tr!('/-','__')
        # remove '$':
        str.delete!('$')
        # remove all other nonnumerical chars:
        str.gsub!(/[^0-9a-zA-Z_]/i, '')
        # sometimes remaining string may be empty:
        str = 'GENERIC_TOPIC' if str.empty?
        # return string as new instance variable name:
        str
end
default_config() click to toggle source

Returns Hash with default config-data for Hodmin. File MUST be edited after creation by user.

# File lib/hodmin/hodmin_tools.rb, line 406
def default_config
  config = {}
  config['mqtt'] = Hash['protocol' => 'mqtt://', 'host' => 'mqtt.example.com', 'port' => '1883',\
                        'user' => 'username', 'password' => 'password', 'base_topic' => 'devices/homie/',\
                        'auth' => true, 'timeout' => 0.3]
  config['firmware'] = Hash['dir' => '/home/user/sketchbook/', 'filepattern' => '*.bin']
  config['logging'] = Hash['logdestination' => 'nil']
  config['output'] = Hash['list' => 'HD.mac HD.online HD.localip HD.name FW.checksum'\
                   + 'FW.fw_name FW.fw_version HD.upgradable', 'nil' => '']
  config
end
default_config_initialize() click to toggle source

Returns Hash with default config-data to initialize a Homie-device. File MUST be edited after creation by user.

# File lib/hodmin/hodmin_tools.rb, line 419
def default_config_initialize
  config = {}
  config['name'] = 'Homie1234'
  config['wifi'] = Hash['ssid' => 'myWifi', 'password'=>'password']
  config['mqtt'] = Hash['host' => 'myhost.mydomain.local', 'port' => 1883, 'base_topic'=>'devices/homie/'\
           , 'auth'=>true, 'username'=>'user1', 'password' => 'mqttpassword']
  config['ota'] = Hash['enabled' => true]
  config
end
ensure_individual_instance_name(name,list) click to toggle source
# File lib/hodmin/hodmin_tools.rb, line 447
def ensure_individual_instance_name(name,list)
        instance_variable_names = list.map do |i|
                i = i.to_s
                i[0] = '' if i[0] == '@'
                i
        end
        n = 1
        org_name = name
        while instance_variable_names.include?(name)
                # name already used, so change it a little bit:
                name = org_name + '_' + n.to_s.rjust(3, "0")
                n += 1
                raise "ERR: Too many topics with special chars: #{instance_variable_names.join(', ')}" if n > 999
        end
        name
end
fetch_homie_dev_list(*fw_list) click to toggle source

Return a list of Homie-Devices controlled by given broker.

# File lib/hodmin/hodmin_tools.rb, line 240
def fetch_homie_dev_list(*fw_list)
  client = mqtt_connect
  base_topic = configatron.mqtt.base_topic + '#'
  client.subscribe(base_topic)
  list = get_homies(client, fw_list)
  client.disconnect
  list
end
fetch_homie_fw_list() click to toggle source

Return a list of Homie-firmwares found in given diretory-tree. Firmwares are identfied by Magic-byte (see homie-esp8266.readme.io/v2.0.0/docs/magic-bytes). Filenames are ignored, you can specify a pattern in hodmin-config to speed up searching. Default filename-pattern is '*.bin'

# File lib/hodmin/hodmin_tools.rb, line 253
def fetch_homie_fw_list
  directory = configatron.firmware.dir + '**/' + configatron.firmware.filepattern
  Log.log.info "Scanning dir: #{directory}"
  binlist = Dir[directory]
  fw_list = []
  binlist.each do |fw|
    fw_list << FirmwareHomie.new(fw) if homie_firmware?(fw)
  end
  fw_list
end
get_config_from_option(cline) click to toggle source
# File lib/hodmin/hodmin_push_config.rb, line 62
def get_config_from_option(cline)
  # Example: cline = '{"ota":{"enabled":"true"}, "wifi":{"ssid":"abc", "password":"secret"}}'
  return '' if cline.to_s.strip.empty?
  JSON.parse(cline)
end
get_homies(client, *fw_list) click to toggle source

Reads all Homie-Devices from given broker. To be called with connected MQTT-client. Topic has to be set in calling program. Variable timeout_seconds defines, after what time our client.get will be cancelled. Choose a value high enough for your data, but fast enough for quick response. default is 0.7 sec, which should be enough for a lot of Homies controlled by a broker running on a Raspberry-PI.

# File lib/hodmin/hodmin_tools.rb, line 219
def get_homies(client, *fw_list)
  allmqtt = []
  begin
    Timeout.timeout(configatron.mqtt.timeout.to_f) do
      client.get { |topic, message| allmqtt << [topic, message] }
    end
  # we want to read all published messages right now and then leave (otherwise we are blocked)
  rescue Timeout::Error
  end
  # find all homie-IDs (MAC-addresses)
  macs = allmqtt.select { |t, _m| t.include?('/$homie') }.map { |t, _m| t.split('/$').first.split('/').last }
  # create a array of homie-devices for macs in our list:
  homies = []
  macs.each do |mac|
    mqtt = allmqtt.select { |t, _m| t.include?(mac) }
    homies << HomieDevice.new(mqtt, fw_list)
  end
  homies
end
hodmin_initialize(gopts, copts) click to toggle source

Initiate a Homie-Device with first config from a YAML-file. Uses bash CLI calling Curl-binary. Will not work, if curl is not available. Perhaps a better solution should use http-requests => to be done Status: experimental

# File lib/hodmin/hodmin_initialize.rb, line 5
def hodmin_initialize(gopts, copts)
  c1 = 'command -v curl >/dev/null 2>&1 || { echo  "curl required but it is not installed."; }'
  ip = '192.168.123.1'
  ans = `#{c1}`.to_s.strip
  if ans.empty?
    default_filename = 'homie-initialize.yaml'
    filename = copts[:configfile_given] ? copts[:configfile] : default_filename

    unless File.exists?(filename)
      puts "ERR: Configfile with initializing data not found: #{filename}"
      exit if filename != default_filename
      # create example config-file:
      File.open(filename, 'w') { |f| f.puts default_config_initialize.to_yaml }
      puts "WARN: Default initializing data written to: #{filename}. Please edit this file!"
      exit
    end
    
    # write config in JSON-Format to tempfile:
    tempfile = 'configHOMIEjson.tmp'
    File.open(tempfile,'w'){|f| f.puts YAML.load_file(filename).to_json}
    
    # upload to device:
    puts "trying to connect to #{ip} ..."
    c2 = "curl -X PUT http://#{ip}/config -d @#{tempfile} --header 'Content-Type: application/json'"
    ans = `#{c2}`.to_s
    json = JSON.parse(ans)
    if json['success']
      puts "\nDevice is initialized now."
    else
      puts "\nOops. Something went wrong: curl answered: #{ans}"
    end
    File.delete(tempfile)
  else
    # curl not installed
    puts 'ERR: curl required, but it is not installed. Aborting.'
    exit
  end
end
hodmin_list(gopts, copts) click to toggle source

Homie-Admin LIST Print list of Homie-Decices with installed firmware and available firmware in our repo

# File lib/hodmin/hodmin_list.rb, line 3
def hodmin_list(gopts, copts)
  all_fws = fetch_homie_fw_list # we need it for checking upgrade-availability
  my_fws = all_fws.select_by_opts(copts)\
                  .sort do |a, b|
                    [a.fw_brand, a.fw_name, b.fw_version] <=> \
                      [b.fw_brand, b.fw_name, a.fw_version]
                  end
                  
  # fetch all devices, set upgradable-attribute based on my_fws:
  my_devs = fetch_homie_dev_list(my_fws).select_by_opts(gopts)
  my_list = []
  already_listed = []

  my_devs.each do |d|
    firmware = my_fws.select { |f| f.checksum == d.fw_checksum }
    if firmware.count > 0
      # found installed firmware
      my_list << HomiePair.new(d, firmware)
      already_listed << firmware.first.checksum # remember this firmware as already listed
    else
      # did not find firmware-file installed on this device
      my_list << HomiePair.new(d, nil) unless gopts[:upgradable_given]
    end
  end

  # now append remaining firmwares (for which we did not find any Homie running this) to my_list:
  already_listed.uniq!
  my_fws.select { |f| !already_listed.include?(f.checksum) }.each { |f| my_list << HomiePair.new(nil, f) }

  # attributes of my_list we want to see in output:
  # HD: attributes coming from HomieDevice
  # FW: attributes coming from firmware-file
  # AD: additional attributes in HomiePair-class for special purposes
  # Read our format for table from config-file:
  attribs = configatron.output.list.strip.split(/ /) unless configatron.output.nil?
  
  # create output-table
  rows = my_list.create_output_table(attribs, copts[:style])
  # define a header for our output-table
  # header = attribs.map { |a| a.gsub(/HD./, '').gsub(/FW./, '').gsub(/AD./, '') }
  header = attribs.map(&:setup_header)
  # build table object:
  output = TTY::Table.new header, rows
  table_style = copts[:style_given] ? copts[:style].to_sym : :unicode # :ascii :basic
  
  # show our table:
  puts output.render(table_style, alignment: copts[:style] == 'basic' ? [:left] : [:center])
end
hodmin_pull_config(gopts, copts) click to toggle source

pullCF reads Homie-device config-data via mqtt-protocol. Output-format is YAML

# File lib/hodmin/hodmin_pull_config.rb, line 3
def hodmin_pull_config(gopts, copts)
  my_devs = fetch_homie_dev_list.select_by_opts(gopts)

  my_devs.each do |pull_dev|
    if copts[:outputfile_given]
      filename = pull_dev.config_yaml_filename_homie(copts[:outputfile])
      File.open(filename, 'w') do |f|
        f.puts "# YAML Configfile written by hodmin Version #{configatron.VERSION}"
        f.puts "# MAC: #{pull_dev.mac}"
        f.puts "# Status during pullCF: #{pull_dev.online_status}"
        f.puts "# #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
        f.puts pull_dev.implementation_config.config_from_string
      end
    else
      puts "Config of device #{pull_dev.name} (#{pull_dev.mac}):"
      puts pull_dev.implementation_config.config_from_string
    end
  end
end
hodmin_push_config(gopts, copts) click to toggle source

Pushes a config to Homie-device via MQTT-Broker.

# File lib/hodmin/hodmin_push_config.rb, line 2
def hodmin_push_config(gopts, copts)
  conf_cmd  = copts[:jsonconfig] || ''
  conf_file = copts[:inputfile] || ''
  conf_short = copts[:shortconfig] || ''

  number_of_options = [:jsonconfig_given, :inputfile_given, :shortconfig_given]\
                      .count { |e| copts.keys.include?(e) }

  unless number_of_options == 1
    puts 'ERR: please specify exactly ONE option of: -s, -j, -i'
    return
  end

  if copts[:inputfile_given] && !File.exist?(conf_file)
    puts "ERR: File not found: #{conf_file}"
    return
  end

  conf_file = YAML.load_file(conf_file).to_json if copts[:inputfile_given]

  conf_new = conf_cmd if copts[:jsonconfig_given]
  conf_new = conf_file if copts[:inputfile_given]
  conf_new = options_long(conf_short) if copts[:shortconfig_given]

  if conf_new.empty?
    puts 'ERR: No valid config-options found.'
    return
  end

  my_devs = fetch_homie_dev_list.select_by_opts(gopts)

  my_devs.each do |up_dev|
    copts = get_config_from_option(conf_new)
    puts "Device #{up_dev.mac} is #{up_dev.online_status}"
    next unless up_dev.online?
    print 'Start updating? <Yn>:'
    answer = STDIN.gets.chomp.downcase
    next unless up_dev.online && 'y' == answer
    client = mqtt_connect
    base_topic = configatron.mqtt.base_topic + up_dev.mac + '/'
    client.subscribe(base_topic + '$implementation/config')
    conf_old = ''
    client.get do |_topic, message|
      # wait for next message in our queue:
      if conf_old == ''
        # first loop, store existing config to compare after update:
        conf_old = message # we do need message only
        client.publish(base_topic + '$implementation/config/set', copts.to_json, retain: false)
        puts 'done, device reboots, waiting for ACK...'
      else
        # we received a new config
        new_conf = message
        break if JSON.parse(new_conf).values_at(*copts.keys) == copts.values
      end
      puts "ACK received, device #{up_dev.mac} rebooted with new config."
    end
    client.disconnect
  end
end
hodmin_push_firmware(gopts, copts) click to toggle source

Uploads firmware to Homie-Device(s)

# File lib/hodmin/hodmin_push_firmware.rb, line 2
def hodmin_push_firmware(gopts, copts)
  fw_checksum = copts[:checksum] || ''
  fw_name = copts[:fw_name] || ''
  batchmode = copts[:auto] || false
  mac = gopts[:mac] || ''
  hd_upgrade = gopts[:upgradable_given] && gopts[:upgradable]
  fw_upgrade = copts[:upgrade_given] && copts[:upgrade]

  gopts[:mac] = mac = '*' if hd_upgrade && gopts[:mac_given]

  if fw_checksum.empty? && fw_name.empty?
    puts "ERR: No valid firmware-referrer found. (Chksum:#{fw_checksum}, Name: #{fw_name})"
    return
  end
  unless (!fw_checksum.empty? && fw_name.empty?) || (fw_checksum.empty? && !fw_name.empty?)
    puts 'ERR: Please specify firmware either by checksum or by name (for newest of this name).'
    return
  end

  unless !mac.empty? || !fw_name.empty?
    puts 'ERR: No valid device specified.'
    return
  end

  # first find our firmware:
  my_fws = fetch_homie_fw_list.select_by_opts(copts)
                              .sort { |a, b| [a.fw_name, b.fw_version] <=> [b.fw_name, a.fw_version] }

  if my_fws.empty?
    puts 'ERR: None of available firmwares does match this pattern'
    return
  else
    if my_fws.size > 1 && !fw_upgrade
      puts 'ERR: Firmware specification is ambigous'
      return
    end
  end

  # only first firmware selected for pushing:
  my_fw = my_fws.first

  # now find our device(s)
  my_devs = fetch_homie_dev_list(my_fws).select_by_opts(gopts)

  return if my_devs.empty?

  my_devs.each do |up_dev|
    next if hd_upgrade && !up_dev.upgradable
    my_fw = my_fws.select { |f| f.fw_name == up_dev.fw_name }.sort_by(&:fw_version).last if hd_upgrade
    puts "Device #{up_dev.mac} is #{up_dev.online_status}. (installed FW-Checksum: #{up_dev.fw_checksum})"
    next unless up_dev.online? && up_dev.fw_checksum != my_fw.checksum
    if batchmode
      answer = 'y'
    else
      print "New firmware: #{my_fw.checksum}. Start pushing? <Yn>:"
      answer = STDIN.gets.chomp.downcase
    end
    Log.log.info "Dev. #{up_dev.mac} (running #{up_dev.fw_version}) upgrading to #{my_fw.fw_version}"
    up_dev.push_firmware_to_dev(my_fw) if 'y' == answer
  end
end
hodmin_remove(copts) click to toggle source
# File lib/hodmin/hodmin_remove.rb, line 1
def hodmin_remove(copts)
  fw_checksum = copts[:checksum] || ''
  fw_name = copts[:fw_name] || ''

  if fw_checksum.empty? && fw_name.empty?
    puts "ERR: No valid firmware-referrer found. (Chksum:#{fw_checksum}, Name: #{fw_name})"
    return
  end
  unless (!fw_checksum.empty? && fw_name.empty?) || (fw_checksum.empty? && !fw_name.empty?)
    puts 'ERR: Please specify firmware either by checksum or by name (for newest of this name).'
    return
  end

  # first find our firmware-files:
  my_fws = fetch_homie_fw_list.select_by_opts(copts)
                              .sort { |a, b| [a.fw_name, b.fw_version] <=> [b.fw_name, a.fw_version] }

  if my_fws.empty?
    puts 'ERR: None of available firmwares does match this pattern'
    return
  else
    my_fws.each do |f|
      puts "found Fw: Name: #{f.fw_name}, Version: #{f.fw_version}, MD5: #{f.checksum}"
    end
  end

  my_fws.each do |my_fw|
    print "Remove firmware: #{my_fw.fw_name}, #{my_fw.fw_version}, #{my_fw.checksum}. Remove now? <Yn>:"
    answer = STDIN.gets.chomp.downcase
    File.delete(my_fw.file_path) if 'y' == answer
  end
end
hodmin_rename(_gopts, copts) click to toggle source
# File lib/hodmin/hodmin_rename.rb, line 1
def hodmin_rename(_gopts, copts)
  fw_checksum = copts[:checksum] || ''
  fw_name = copts[:fw_name] || ''

  if fw_checksum.empty? && fw_name.empty?
    puts "ERR: No valid firmware-referrer found. (Chksum:#{fw_checksum}, Name: #{fw_name})"
    return
  end
  unless (!fw_checksum.empty? && fw_name.empty?) || (fw_checksum.empty? && !fw_name.empty?)
    puts 'ERR: Please specify firmware either by checksum or by name (for newest of this name).'
    return
  end

  # first find our firmware-files:
  my_fws = fetch_homie_fw_list.select_by_opts(copts)
                              .sort { |a, b| [a.fw_name, b.fw_version] <=> [b.fw_name, a.fw_version] }

  if my_fws.empty?
    puts 'ERR: None of available firmware does match this pattern'
    return
  else
    my_fws.each do |f|
      puts "found Fw: Name: #{f.fw_name}, Version: #{f.fw_version}, MD5: #{f.checksum}"
    end
  end

  my_fws.each do |my_fw|
    bin_pattern = "Homie_#{my_fw.fw_name}_#{my_fw.fw_version}_#{my_fw.checksum}.bin"
    fileobj = Pathname.new(my_fw.file_path)
    next if bin_pattern == Pathname.new(my_fw.file_path).basename.to_s
    puts "Rename firmware: #{my_fw.fw_name}, #{my_fw.fw_version}, #{my_fw.checksum}."
    print "Rename to #{bin_pattern}? <Yn>:"
    answer = STDIN.gets.chomp.downcase
    fileobj.rename(fileobj.dirname + bin_pattern) if 'y' == answer
  end
end
homie_firmware?(filename) click to toggle source

Searches within a binaryfile for so called Homie-magic-bytes to detect a Homie-firmware. See homie-esp8266.readme.io/v2.0.0/docs/magic-bytes

# File lib/hodmin/hodmin_tools.rb, line 73
def homie_firmware?(filename)
  # returns TRUE, if Homiepattern is found inside binary
  binfile = IO.binread(filename).unpack('H*').first
  homie_pattern = "\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25".unpack('H*').first
  binfile.include?(homie_pattern)
end
mqtt_connect() click to toggle source

Methods connects to a MQTT-broker and returns an object linking to this connection.

# File lib/hodmin/hodmin_tools.rb, line 81
def mqtt_connect
  # establish connection for publishing, return client-object
  credentials = configatron.mqtt.auth ? configatron.mqtt.user + ':' + configatron.mqtt.password + '@' : ''
  connection = configatron.mqtt.protocol + credentials + configatron.mqtt.host
  begin
    MQTT::Client.connect(connection, configatron.mqtt.port)
  rescue MQTT::ProtocolException
    puts "ERR: Username and / or password wrong?\n#{connection} at port #{configatron.mqtt.port}"
    exit
  end
end
options_long(short) click to toggle source

Returns JSON-String with key-value pairs depending on input-string. Example: hodmin pushCF -s “name:test-esp8266 ota:on ssid:xy wifipw:xy host:xy port:xy base_topic:xy auth:off user:xy mqttpw:xy” Enclose multiple options in “”, separate options with a blank

# File lib/hodmin/hodmin_push_config.rb, line 72
def options_long(short)
  list = short.split(/ /)
  cfg = { 'wifi' => {}, 'mqtt' => {} }
  list.each do |o|
    key, value = o.split(/:/).map(&:strip)
    case key.downcase
    when 'name' then cfg['name'] = value
    when 'ssid' then cfg['wifi'] = Hash['ssid' => value]
    when 'wifipw' then cfg['wifi'] = Hash['password' => value]
    when 'host' then cfg['mqtt'] << Hash['host' => value]
    when 'port' then cfg['mqtt'] << Hash['port' => value]
    when 'base_topic' then cfg['mqtt'] = Hash['base_topic' => value]
    when 'auth' then cfg['mqtt'] = cfg['mqtt'].merge(Hash['auth' => value == 'on' ? true : false])
    when 'user' then cfg['mqtt'] << Hash['username' => value]
    when 'mqttpw' then cfg['mqtt'] = cfg['mqtt'].merge(Hash['password' => value])
    when 'ota' then cfg['ota'] = Hash['enabled' => value == 'on' ? true : false]
#   to be done:
#    when 'settings' then
#           puts "to be done: settings not implemented in short-config right now"
#           puts "key=#{key}, value=#{value}"
    else
      puts "ERR: illegal option: #{key.downcase}"
      exit
    end
  end

  cfg = cfg.delete_if { |_k, v| v.nil? || v.empty? }
  puts "\nNew config will be: #{cfg.inspect}"
  cfg.to_json
end
upgradable?(fw_name, fw_version, fw_list) click to toggle source

Check a firmware against available bin-files of Homie-firmwares. Returns true, if there is a higher Version than installed. Returns false, if there is no suitable firmware-file found or installed version is the highest version found.

# File lib/hodmin/hodmin_tools.rb, line 351
def upgradable?(fw_name, fw_version, fw_list)
  fw_list.flatten!
  # select highest Version of fw_name from given firmware_list:
  return false if fw_list.empty? # No entries in Softwarelist
  best_version = fw_list.select { |h| h.fw_name == fw_name }\
                        .sort_by(&:fw_version).last
  best_version.nil? ? false : fw_version < best_version.fw_version
end