class Object
Public Instance Methods
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
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
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
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
# 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
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
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
# 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
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
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
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
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
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
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
# 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
# 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
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
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
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
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