hodmin 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ca64ac3ab89207e68d267256ed0a4ff2f470a4f6
4
+ data.tar.gz: 9fe8e38d5b38c415e6dc7c0e684d4edf11bb6953
5
+ SHA512:
6
+ metadata.gz: 616baeec41f7eefafc5f7b7a593dddc8d7ec23fb3db92a7ae5382bc68388133e7b5f17077f187d2c2a915deae6d78265465be341c56488bbdff20c92570babfd
7
+ data.tar.gz: e2e69c449d0a01e20ac92f00d609811f6fed9f8034cde446deed91bd048f9781b31636160c5645940b8a2a941c4a177098b1168aa8324abaa4d026329fc84d62
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env ruby
2
+ # hodmin (homie-admin): provides cli to administrate homie-devices
3
+
4
+ require 'trollop'
5
+ require 'digest/md5'
6
+ require 'pathname'
7
+ require 'tty/table'
8
+ require 'tty/cursor'
9
+ require 'pastel'
10
+ require 'json'
11
+ require 'logger'
12
+ require 'yaml'
13
+ require 'configatron'
14
+ require 'mqtt'
15
+ require 'hodmin.rb'
16
+
17
+ require 'hodmin/hodmin_list.rb'
18
+ require 'hodmin/hodmin_push_config.rb'
19
+ require 'hodmin/hodmin_pull_config.rb'
20
+ require 'hodmin/hodmin_push_firmware.rb'
21
+ require 'hodmin/hodmin_remove.rb'
22
+ require 'hodmin/hodmin_rename.rb'
23
+ require 'hodmin/hodmin_tools.rb'
24
+ require 'hodmin/hodmin_initialize.rb'
25
+
26
+ MAX_HOMIEVERSION = '2.0.0'.freeze
27
+ COMMANDS = [%w(list pushFW pushCF initialize pullCF remove rename help)].freeze
28
+ global_opts = Trollop.options do
29
+ banner <<-EOS.gsub(/ /, '').gsub(/#VS#/, Hodmin::VERSION)
30
+ HODMIN - Homie-Administration utility for using Homie-devices (ESP8266 controlled microcomputers)
31
+ Version #VS# (C) T. Romeyke
32
+ Usage:
33
+ hodmin [OPTIONS] COMMAND [SUBOPTIONS]
34
+
35
+ COMMANDS
36
+ list list Homie-devices and / or firmware
37
+ pushCF push config-data to device
38
+ pullCF pull config-data from device
39
+ pushFW push firmware-file to device (OTA: Over-The-Air, WIFI)
40
+ rename rename firmware-file in firmware-directory
41
+ remove remove firmware-file from firmware-directory
42
+ initialize initialize Homie-device after first flashing of firmware via USB. Uses WIFI-AP of Device. EXPERIMENTAL!
43
+
44
+ Options (before COMMAND)
45
+ Every option-string can be shorted with a star: ie: --mac c55* OR *c55 => all mac starting/ending with c55
46
+ where [OPTIONS] are:
47
+ EOS
48
+ opt :mac, 'select device(s) by mac', type: String
49
+ opt :fw_name, 'select device(s) by firmware-name', type: String
50
+ opt :checksum, 'select device(s) by actual firmware-checksum', type: String
51
+ opt :localip, 'select device(s) by actual ip4-address', type: String
52
+ opt :configfile, 'use this configfile instead of default', type: String, default: '~/.hodmin.yaml'
53
+ stop_on COMMANDS
54
+ end
55
+
56
+ cmd = ARGV.shift # get the command
57
+ cmd_opts = case cmd
58
+ when 'list' # parse list options
59
+ Trollop.options do
60
+ opt :fw_name, 'Select firmware(s) by firmware-name', type: String
61
+ opt :checksum, 'Select firmware(s) by firmware-checksum', type: String
62
+ opt :style, 'Output table style: unicode, ascii or basic', type: String
63
+ opt :nil, 'Text to be printed in table in case of NIL-value', type: String
64
+ end
65
+ when 'pushFW' # parse pushFW options
66
+ Trollop.options do
67
+ opt :fw_name, 'Select firmware-file by firmware-name', type: String
68
+ opt :checksum, 'Select ONE firmware-files by firmware-checksum', type: String
69
+ opt :upgrade, 'Select newest firmware-file by firmware-name of Homie-device', type: TrueClass
70
+ opt :auto, 'Upgrade in batch mode (do not ask for updating a device'\
71
+ + ' (be carefull using this option)', type: TrueClass, default: false
72
+ end
73
+ when 'pushCF' # parse pushCF options
74
+ Trollop.options do
75
+ opt :jsonconfig, 'JSON-formatted string with config-option(s) to change', short: '-j', type: String
76
+ opt :inputfile, 'Read new config-options from YAML-file', type: String
77
+ opt :shortconfig, 'Fast changing of some config-options. Only name:xy ota:on|off'\
78
+ + ' ssid:xy wifipw:xy host:xy port:xy base_topic:xy'\
79
+ + ' auth:on|off user:xy mqttpw:xy.'\
80
+ + ' Enclose multiple options in "", separate options with a blank.'\
81
+ + ' Arguments (i.e. passwords) must NOT include blanks or colons.'\
82
+ + ' If you need these characters, use option -j or -i.', type: String
83
+ end
84
+ when 'pullCF' # parse pullCF options
85
+ Trollop.options do
86
+ opt :outputfile, 'Save config-options to file', type: String, default: 'Homie-<MAC>'
87
+ end
88
+ when 'initialize' # parse initialize options
89
+ Trollop.options do
90
+ opt :configfile, 'Push initiating config-options from file to new device'\
91
+ , type: String, default: 'homie-initialize.yaml'
92
+ end
93
+ when 'remove' # remove binary file
94
+ Trollop.options do
95
+ opt :fw_name, 'Select firmware-file by firmware-name', type: String
96
+ opt :checksum, 'Select firmware-file(s) by firmware-checksum', type: String
97
+ end
98
+ when 'rename' # rename binary file
99
+ Trollop.options do
100
+ opt :fw_name, 'Select firmware-file by firmware-name', type: String
101
+ opt :checksum, 'Select firmware-file(s) by firmware-checksum', type: String
102
+ end
103
+ else
104
+ Trollop.die "unknown subcommand #{cmd.inspect}"
105
+ end
106
+
107
+ configatron.VERSION = Hodmin::VERSION
108
+ configatron.MAX_HOMIEVERSION = MAX_HOMIEVERSION
109
+
110
+ # read config:
111
+ configfile = ''
112
+ if global_opts[:configfile_given]
113
+ # if option -o is given, use config-file named in this option
114
+ configfile = File.expand_path(global_opts[:configfile])
115
+ unless File.exist?(configfile)
116
+ puts "ERR: Configfile not found: #{configfile}"
117
+ exit
118
+ end
119
+ else
120
+ # no special configfile given, so read global hodmin-config of this user.
121
+ # If reading fails, store new hodmin-config with default values and exit.
122
+ configfile = File.expand_path('~/.hodmin.yaml')
123
+ unless File.exist?(configfile)
124
+ File.open(configfile, 'w') { |f| f.puts default_config.to_yaml }
125
+ puts "WARN: Default configfile written to: #{configfile}"
126
+ end
127
+ end
128
+
129
+ # now we can load from configfile
130
+ config = YAML.load_file(configfile)
131
+
132
+ # check config:
133
+ exit unless check_config_ok?(config, configfile)
134
+
135
+ configatron.configure_from_hash(config)
136
+
137
+ # set colors for pastel list output:
138
+ ENV.store('PASTEL_COLORS_ALIASES', 'hd_f=black,hd_b=on_bright_green,fw_f=black,fw_b=on_bright_blue')
139
+
140
+ configatron.output.nil = cmd_opts[:nil] if cmd_opts[:nil_given]
141
+
142
+ case cmd
143
+ when 'list'
144
+ hodmin_list(global_opts, cmd_opts)
145
+ when 'pushFW'
146
+ hodmin_push_firmware(global_opts, cmd_opts)
147
+ when 'pushCF'
148
+ hodmin_push_config(global_opts, cmd_opts)
149
+ when 'pullCF'
150
+ hodmin_pull_config(global_opts, cmd_opts)
151
+ when 'initialize'
152
+ hodmin_initialize(global_opts, cmd_opts)
153
+ when 'remove'
154
+ hodmin_remove(cmd_opts)
155
+ when 'rename'
156
+ hodmin_rename(global_opts, cmd_opts)
157
+ else
158
+ Log.log.error "unknown CMD:#{cmd}. Opts: #{global_opts}"
159
+ end
@@ -0,0 +1,5 @@
1
+ require "hodmin/version"
2
+
3
+ module Hodmin
4
+ # nothing here right now
5
+ end
@@ -0,0 +1,42 @@
1
+ # Initiate a Homie-Device with first config from a YAML-file.
2
+ # Uses bash CLI calling Curl-binary. Will not work, if curl is not available.
3
+ # Perhaps a better solution should use http-requests => to be done
4
+ # Status: experimental
5
+ def hodmin_initialize(gopts, copts)
6
+ c1 = 'command -v curl >/dev/null 2>&1 || { echo "curl required but it is not installed."; }'
7
+ ip = '192.168.123.1'
8
+ ans = `#{c1}`.to_s.strip
9
+ if ans.empty?
10
+ default_filename = 'homie-initialize.yaml'
11
+ filename = copts[:configfile_given] ? copts[:configfile] : default_filename
12
+
13
+ unless File.exists?(filename)
14
+ puts "ERR: Configfile with initializing data not found: #{filename}"
15
+ exit if filename != default_filename
16
+ # create example config-file:
17
+ File.open(filename, 'w') { |f| f.puts default_config_initialize.to_yaml }
18
+ puts "WARN: Default initializing data written to: #{filename}. Please edit this file!"
19
+ exit
20
+ end
21
+
22
+ # write config in JSON-Format to tempfile:
23
+ tempfile = 'configHOMIEjson.tmp'
24
+ File.open(tempfile,'w'){|f| f.puts YAML.load_file(filename).to_json}
25
+
26
+ # upload to device:
27
+ puts "trying to connect to #{ip} ..."
28
+ c2 = "curl -X PUT http://#{ip}/config -d @#{tempfile} --header 'Content-Type: application/json'"
29
+ ans = `#{c2}`.to_s
30
+ json = JSON.parse(ans)
31
+ if json['success']
32
+ puts "\nDevice is initialized now."
33
+ else
34
+ puts "\nOops. Something went wrong: curl answered: #{ans}"
35
+ end
36
+ File.delete(tempfile)
37
+ else
38
+ # curl not installed
39
+ puts 'ERR: curl required, but it is not installed. Aborting.'
40
+ exit
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ # Homie-Admin LIST
2
+ # Print list of Homie-Decices with installed firmware and available firmware in our repo
3
+ def hodmin_list(gopts, copts)
4
+ all_fws = fetch_homie_fw_list # we need it for checking upgrade-availability
5
+ my_fws = all_fws.select_by_opts(copts)\
6
+ .sort do |a, b|
7
+ [a.fw_brand, a.fw_name, b.fw_version] <=> \
8
+ [b.fw_brand, b.fw_name, a.fw_version]
9
+ end
10
+
11
+ # fetch all devices, set upgradable-attribute based on my_fws:
12
+ my_devs = fetch_homie_dev_list(my_fws).select_by_opts(gopts)
13
+ my_list = []
14
+ already_listed = []
15
+
16
+ my_devs.each do |d|
17
+ firmware = my_fws.select { |f| f.checksum == d.fw_checksum }
18
+ if firmware.count > 0
19
+ # found installed firmware
20
+ my_list << HomiePair.new(d, firmware)
21
+ already_listed << firmware.first.checksum # remember this firmware as already listed
22
+ else
23
+ # did not find firmware-file installed on this device
24
+ my_list << HomiePair.new(d, nil) unless gopts[:upgradable_given]
25
+ end
26
+ end
27
+
28
+ # now append remaining firmwares (for which we did not find any Homie running this) to my_list:
29
+ already_listed.uniq!
30
+ my_fws.select { |f| !already_listed.include?(f.checksum) }.each { |f| my_list << HomiePair.new(nil, f) }
31
+
32
+ # attributes of my_list we want to see in output:
33
+ # HD: attributes coming from HomieDevice
34
+ # FW: attributes coming from firmware-file
35
+ # AD: additional attributes in HomiePair-class for special purposes
36
+ # Read our format for table from config-file:
37
+ attribs = configatron.output.list.strip.split(/ /) unless configatron.output.nil?
38
+
39
+ # create output-table
40
+ rows = my_list.create_output_table(attribs, copts[:style])
41
+ # define a header for our output-table
42
+ # header = attribs.map { |a| a.gsub(/HD./, '').gsub(/FW./, '').gsub(/AD./, '') }
43
+ header = attribs.map(&:setup_header)
44
+ # build table object:
45
+ output = TTY::Table.new header, rows
46
+ table_style = copts[:style_given] ? copts[:style].to_sym : :unicode # :ascii :basic
47
+
48
+ # show our table:
49
+ puts output.render(table_style, alignment: copts[:style] == 'basic' ? [:left] : [:center])
50
+ end
51
+
52
+ # Extends class String with methods for using Pastel-Methods
53
+ class String
54
+ def setup_header
55
+ pastel = Pastel.new
56
+ case slice(0, 2)
57
+ when 'HD' then pastel.white(gsub(/HD./, ''))
58
+ when 'FW' then pastel.white(gsub(/FW./, ''))
59
+ else gsub(/AD./, '')
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,42 @@
1
+ # pullCF reads Homie-device config-data via mqtt-protocol.
2
+ # Output-format is YAML
3
+ def hodmin_pull_config(gopts, copts)
4
+ my_devs = fetch_homie_dev_list.select_by_opts(gopts)
5
+
6
+ my_devs.each do |pull_dev|
7
+ if copts[:outputfile_given]
8
+ filename = pull_dev.config_yaml_filename_homie(copts[:outputfile])
9
+ File.open(filename, 'w') do |f|
10
+ f.puts "# YAML Configfile written by hodmin Version #{configatron.VERSION}"
11
+ f.puts "# MAC: #{pull_dev.mac}"
12
+ f.puts "# Status during pullCF: #{pull_dev.online_status}"
13
+ f.puts "# #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
14
+ f.puts pull_dev.implementation_config.config_from_string
15
+ end
16
+ else
17
+ puts "Config of device #{pull_dev.name} (#{pull_dev.mac}):"
18
+ puts pull_dev.implementation_config.config_from_string
19
+ end
20
+ end
21
+ end
22
+
23
+ # Converts string with config-Data (json-format) to nice output.
24
+ class String
25
+ def config_from_string
26
+ return '' if strip.empty?
27
+ JSON.parse(self).to_yaml
28
+ end
29
+ end
30
+
31
+ # Create filename for config-data of a HomieDevice depending on default pattern
32
+ # (Homie-<MAC>.yaml) or a given parameter replacing 'Homie'.
33
+ class HomieDevice
34
+ def config_yaml_filename_homie(fn)
35
+ config_extension = '.yaml'
36
+ if fn.include?('<MAC>')
37
+ fn.gsub(/<MAC>/, mac) + config_extension
38
+ else
39
+ fn.gsub(/[^A-Za-z0-9]/, '') + '-' + mac + config_extension
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,96 @@
1
+ # Pushes a config to Homie-device via MQTT-Broker.
2
+ def hodmin_push_config(gopts, copts)
3
+ conf_cmd = copts[:jsonconfig] || ''
4
+ conf_file = copts[:inputfile] || ''
5
+ conf_short = copts[:shortconfig] || ''
6
+
7
+ number_of_options = [:jsonconfig_given, :inputfile_given, :shortconfig_given]\
8
+ .count { |e| copts.keys.include?(e) }
9
+
10
+ unless number_of_options == 1
11
+ puts 'ERR: please specify exactly ONE option of: -s, -j, -i'
12
+ return
13
+ end
14
+
15
+ if copts[:inputfile_given] && !File.exist?(conf_file)
16
+ puts "ERR: File not found: #{conf_file}"
17
+ return
18
+ end
19
+
20
+ conf_file = YAML.load_file(conf_file).to_json if copts[:inputfile_given]
21
+
22
+ conf_new = conf_cmd if copts[:jsonconfig_given]
23
+ conf_new = conf_file if copts[:inputfile_given]
24
+ conf_new = options_long(conf_short) if copts[:shortconfig_given]
25
+
26
+ if conf_new.empty?
27
+ puts 'ERR: No valid config-options found.'
28
+ return
29
+ end
30
+
31
+ my_devs = fetch_homie_dev_list.select_by_opts(gopts)
32
+
33
+ my_devs.each do |up_dev|
34
+ copts = get_config_from_option(conf_new)
35
+ puts "Device #{up_dev.mac} is #{up_dev.online_status}"
36
+ next unless up_dev.online?
37
+ print 'Start updating? <Yn>:'
38
+ answer = STDIN.gets.chomp.downcase
39
+ next unless up_dev.online && 'y' == answer
40
+ client = mqtt_connect
41
+ base_topic = configatron.mqtt.base_topic + up_dev.mac + '/'
42
+ client.subscribe(base_topic + '$implementation/config')
43
+ conf_old = ''
44
+ client.get do |_topic, message|
45
+ # wait for next message in our queue:
46
+ if conf_old == ''
47
+ # first loop, store existing config to compare after update:
48
+ conf_old = message # we do need message only
49
+ client.publish(base_topic + '$implementation/config/set', copts.to_json, retain: false)
50
+ puts 'done, device reboots, waiting for ACK...'
51
+ else
52
+ # we received a new config
53
+ new_conf = message
54
+ break if JSON.parse(new_conf).values_at(*copts.keys) == copts.values
55
+ end
56
+ puts "ACK received, device #{up_dev.mac} rebooted with new config."
57
+ end
58
+ client.disconnect
59
+ end
60
+ end
61
+
62
+ def get_config_from_option(cline)
63
+ # Example: cline = '{"ota":{"enabled":"true"}, "wifi":{"ssid":"abc", "password":"secret"}}'
64
+ return '' if cline.to_s.strip.empty?
65
+ JSON.parse(cline)
66
+ end
67
+
68
+ # Returns JSON-String with key-value pairs depending on input-string.
69
+ # Example: hodmin pushCF -s "name:test-esp8266 ota:on ssid:xy wifipw:xy host:xy port:xy
70
+ # base_topic:xy auth:off user:xy mqttpw:xy"
71
+ # Enclose multiple options in "", separate options with a blank
72
+ def options_long(short)
73
+ list = short.split(/ /)
74
+ cfg = { 'wifi' => {}, 'mqtt' => {} }
75
+ list.each do |o|
76
+ key, value = o.split(/:/).map(&:strip)
77
+ case key.downcase
78
+ when 'name' then cfg['name'] = value
79
+ when 'ssid' then cfg['wifi'] = Hash['ssid' => value]
80
+ when 'wifipw' then cfg['wifi'] = Hash['password' => value]
81
+ when 'host' then cfg['mqtt'] << Hash['host' => value]
82
+ when 'port' then cfg['mqtt'] << Hash['port' => value]
83
+ when 'base_topic' then cfg['mqtt'] = Hash['base_topic' => value]
84
+ when 'auth' then cfg['mqtt'] = cfg['mqtt'].merge(Hash['auth' => value == 'on' ? true : false])
85
+ when 'user' then cfg['mqtt'] << Hash['username' => value]
86
+ when 'mqttpw' then cfg['mqtt'] = cfg['mqtt'].merge(Hash['password' => value])
87
+ when 'ota' then cfg['ota'] = Hash['enabled' => value == 'on' ? true : false]
88
+ else
89
+ puts "ERR: illegal option: #{key.downcase}"
90
+ exit
91
+ end
92
+ end
93
+ cfg = cfg.delete_if { |_k, v| v.nil? || v.empty? }
94
+ puts "\nNew config will be: #{cfg.inspect}"
95
+ cfg.to_json
96
+ end
@@ -0,0 +1,62 @@
1
+ # Uploads firmware to Homie-Device(s)
2
+ def hodmin_push_firmware(gopts, copts)
3
+ fw_checksum = copts[:checksum] || ''
4
+ fw_name = copts[:fw_name] || ''
5
+ batchmode = copts[:auto] || false
6
+ mac = gopts[:mac] || ''
7
+ hd_upgrade = gopts[:upgradable_given] && gopts[:upgradable]
8
+ fw_upgrade = copts[:upgrade_given] && copts[:upgrade]
9
+
10
+ gopts[:mac] = mac = '*' if hd_upgrade && gopts[:mac_given]
11
+
12
+ if fw_checksum.empty? && fw_name.empty?
13
+ puts "ERR: No valid firmware-referrer found. (Chksum:#{fw_checksum}, Name: #{fw_name})"
14
+ return
15
+ end
16
+ unless (!fw_checksum.empty? && fw_name.empty?) || (fw_checksum.empty? && !fw_name.empty?)
17
+ puts 'ERR: Please specify firmware either by checksum or by name (for newest of this name).'
18
+ return
19
+ end
20
+
21
+ unless !mac.empty? || !fw_name.empty?
22
+ puts 'ERR: No valid device specified.'
23
+ return
24
+ end
25
+
26
+ # first find our firmware:
27
+ my_fws = fetch_homie_fw_list.select_by_opts(copts)
28
+ .sort { |a, b| [a.fw_name, b.fw_version] <=> [b.fw_name, a.fw_version] }
29
+
30
+ if my_fws.empty?
31
+ puts 'ERR: None of available firmwares does match this pattern'
32
+ return
33
+ else
34
+ if my_fws.size > 1 && !fw_upgrade
35
+ puts 'ERR: Firmware specification is ambigous'
36
+ return
37
+ end
38
+ end
39
+
40
+ # only first firmware selected for pushing:
41
+ my_fw = my_fws.first
42
+
43
+ # now find our device(s)
44
+ my_devs = fetch_homie_dev_list(my_fws).select_by_opts(gopts)
45
+
46
+ return if my_devs.empty?
47
+
48
+ my_devs.each do |up_dev|
49
+ next if hd_upgrade && !up_dev.upgradable
50
+ my_fw = my_fws.select { |f| f.fw_name == up_dev.fw_name }.sort_by(&:fw_version).last if hd_upgrade
51
+ puts "Device #{up_dev.mac} is #{up_dev.online_status}. (installed FW-Checksum: #{up_dev.fw_checksum})"
52
+ next unless up_dev.online? && up_dev.fw_checksum != my_fw.checksum
53
+ if batchmode
54
+ answer = 'y'
55
+ else
56
+ print "New firmware: #{my_fw.checksum}. Start pushing? <Yn>:"
57
+ answer = STDIN.gets.chomp.downcase
58
+ end
59
+ Log.log.info "Dev. #{up_dev.mac} (running #{up_dev.fw_version}) upgrading to #{my_fw.fw_version}"
60
+ up_dev.push_firmware_to_dev(my_fw) if 'y' == answer
61
+ end
62
+ end
@@ -0,0 +1,32 @@
1
+ def hodmin_remove(copts)
2
+ fw_checksum = copts[:checksum] || ''
3
+ fw_name = copts[:fw_name] || ''
4
+
5
+ if fw_checksum.empty? && fw_name.empty?
6
+ puts "ERR: No valid firmware-referrer found. (Chksum:#{fw_checksum}, Name: #{fw_name})"
7
+ return
8
+ end
9
+ unless (!fw_checksum.empty? && fw_name.empty?) || (fw_checksum.empty? && !fw_name.empty?)
10
+ puts 'ERR: Please specify firmware either by checksum or by name (for newest of this name).'
11
+ return
12
+ end
13
+
14
+ # first find our firmware-files:
15
+ my_fws = fetch_homie_fw_list.select_by_opts(copts)
16
+ .sort { |a, b| [a.fw_name, b.fw_version] <=> [b.fw_name, a.fw_version] }
17
+
18
+ if my_fws.empty?
19
+ puts 'ERR: None of available firmwares does match this pattern'
20
+ return
21
+ else
22
+ my_fws.each do |f|
23
+ puts "found Fw: Name: #{f.fw_name}, Version: #{f.fw_version}, MD5: #{f.checksum}"
24
+ end
25
+ end
26
+
27
+ my_fws.each do |my_fw|
28
+ print "Remove firmware: #{my_fw.fw_name}, #{my_fw.fw_version}, #{my_fw.checksum}. Remove now? <Yn>:"
29
+ answer = STDIN.gets.chomp.downcase
30
+ File.delete(my_fw.file_path) if 'y' == answer
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ def hodmin_rename(_gopts, copts)
2
+ fw_checksum = copts[:checksum] || ''
3
+ fw_name = copts[:fw_name] || ''
4
+
5
+ if fw_checksum.empty? && fw_name.empty?
6
+ puts "ERR: No valid firmware-referrer found. (Chksum:#{fw_checksum}, Name: #{fw_name})"
7
+ return
8
+ end
9
+ unless (!fw_checksum.empty? && fw_name.empty?) || (fw_checksum.empty? && !fw_name.empty?)
10
+ puts 'ERR: Please specify firmware either by checksum or by name (for newest of this name).'
11
+ return
12
+ end
13
+
14
+ # first find our firmware-files:
15
+ my_fws = fetch_homie_fw_list.select_by_opts(copts)
16
+ .sort { |a, b| [a.fw_name, b.fw_version] <=> [b.fw_name, a.fw_version] }
17
+
18
+ if my_fws.empty?
19
+ puts 'ERR: None of available firmware does match this pattern'
20
+ return
21
+ else
22
+ my_fws.each do |f|
23
+ puts "found Fw: Name: #{f.fw_name}, Version: #{f.fw_version}, MD5: #{f.checksum}"
24
+ end
25
+ end
26
+
27
+ my_fws.each do |my_fw|
28
+ bin_pattern = "Homie_#{my_fw.fw_name}_#{my_fw.fw_version}_#{my_fw.checksum}.bin"
29
+ fileobj = Pathname.new(my_fw.file_path)
30
+ next if bin_pattern == Pathname.new(my_fw.file_path).basename.to_s
31
+ puts "Rename firmware: #{my_fw.fw_name}, #{my_fw.fw_version}, #{my_fw.checksum}."
32
+ print "Rename to #{bin_pattern}? <Yn>:"
33
+ answer = STDIN.gets.chomp.downcase
34
+ fileobj.rename(fileobj.dirname + bin_pattern) if 'y' == answer
35
+ end
36
+ end
@@ -0,0 +1,423 @@
1
+ # Defines a comfortable way to use logging.
2
+ class Log
3
+ def self.log
4
+ if @logger.nil?
5
+ log = case configatron.logging.logdestination
6
+ when 'STDOUT' then STDOUT
7
+ when 'nil' then nil
8
+ else configatron.logging.logdestination.to_s
9
+ end
10
+ @logger = Logger.new(log)
11
+ @logger.level = Logger::DEBUG
12
+ @logger.datetime_format = '%Y-%m-%d %H:%M:%S '
13
+ end
14
+ @logger
15
+ end
16
+ end
17
+
18
+ # Defines a class for storing all attributes of a Homie-firmware.
19
+ # Check kind of esp8266-firmware: Homie (>= V.2.0 because of magic bytes)
20
+ # Homie: see https://github.com/marvinroger/homie-esp8266
21
+ # Returns hash with fw-details if <filename> includes magic bytes,
22
+ # returns empty hash if doesn't
23
+ class FirmwareHomie < File
24
+ attr_reader :checksum, :fw_name, :fw_version, :fw_brand, :file_path
25
+
26
+ # Initialize a firmware-file for Homie-device. File is recognized by so called Homie-patterns (strings
27
+ # in binary file) that can be injected through sourcecode.
28
+ # Possible patterns are firmware-name, firmware-version and firmware-brand.
29
+ def initialize(filename)
30
+ @firmware_homie = false
31
+ return unless homie_firmware?(filename)
32
+ @file_path = filename
33
+ @firmware_homie = true
34
+ binfile = IO.binread(filename)
35
+ @checksum = Digest::MD5.hexdigest(binfile)
36
+ binfile = binfile.unpack('H*').first
37
+
38
+ fw_name_pattern = ["\xbf\x84\xe4\x13\x54".unpack('H*').first, "\x93\x44\x6b\xa7\x75".unpack('H*').first]
39
+ fw_version_pattern = ["\x6a\x3f\x3e\x0e\xe1".unpack('H*').first, "\xb0\x30\x48\xd4\x1a".unpack('H*').first]
40
+ fw_brand_pattern = ["\xfb\x2a\xf5\x68\xc0".unpack('H*').first, "\x6e\x2f\x0f\xeb\x2d".unpack('H*').first]
41
+
42
+ @fw_brand = @fw_name = @fw_version = '<none>'
43
+ # find Firmware-Branding
44
+ @fw_brand = fw_brand_pattern.find_pattern(binfile)
45
+ # find Firmware-Name:
46
+ @fw_name = fw_name_pattern.find_pattern(binfile)
47
+ # find Firmware-Version:
48
+ @fw_version = fw_version_pattern.find_pattern(binfile)
49
+ Log.log.info "FW found: #{@fw_name}, #{@fw_version}, #{@checksum}"
50
+ end
51
+
52
+ def homie?
53
+ @firmware_homie
54
+ end
55
+ end
56
+
57
+ # Searches inside binfile for Homie-patterns and returns a string with
58
+ # firmware-brand, name or version.
59
+ class Array
60
+ def find_pattern(binfile)
61
+ result = ''
62
+ if binfile.include?(first)
63
+ result = binfile.split(first)[1].split(last).first.to_s
64
+ result = [result].pack('H*') unless result.empty?
65
+ end
66
+ result
67
+ end
68
+ end
69
+
70
+ # Searches within a binaryfile for so called Homie-magic-bytes to detect
71
+ # a Homie-firmware.
72
+ # See https://homie-esp8266.readme.io/v2.0.0/docs/magic-bytes
73
+ def homie_firmware?(filename)
74
+ # returns TRUE, if Homiepattern is found inside binary
75
+ binfile = IO.binread(filename).unpack('H*').first
76
+ homie_pattern = "\x25\x48\x4f\x4d\x49\x45\x5f\x45\x53\x50\x38\x32\x36\x36\x5f\x46\x57\x25".unpack('H*').first
77
+ binfile.include?(homie_pattern)
78
+ end
79
+
80
+ # Methods connects to a MQTT-broker and returns an object linking to this connection.
81
+ def mqtt_connect
82
+ # establish connection for publishing, return client-object
83
+ credentials = configatron.mqtt.auth ? configatron.mqtt.user + ':' + configatron.mqtt.password + '@' : ''
84
+ connection = configatron.mqtt.protocol + credentials + configatron.mqtt.host
85
+ begin
86
+ MQTT::Client.connect(connection, configatron.mqtt.port)
87
+ rescue MQTT::ProtocolException
88
+ puts "ERR: Username and / or password wrong?\n#{connection} at port #{configatron.mqtt.port}"
89
+ exit
90
+ end
91
+ end
92
+
93
+ # Defines a class for storing all attributes of a Homie-device (aka ESP8266 with
94
+ # Homie-Firmware 2.0 (see: https://github.com/marvinroger/homie).
95
+ class HomieDevice
96
+ attr_reader :mac, :checksum, :fw_brand, :fw_name, :fw_version, :upgradable
97
+ def initialize(mqtt, *fw_list)
98
+ fw_list.flatten!
99
+ startseq = mqtt.select { |t, _m| t.include?('/$homie') }.first.first.split('$').first
100
+ @mac = startseq.split(/\//).last
101
+ mqtt.map! { |t, m| [t.gsub(startseq, ''), m] }
102
+ mhash = Hash[*mqtt.flatten]
103
+ create_attr('homieVersion', mhash['$homie'])
104
+ if mhash['$homie'] > configatron.MAX_HOMIEVERSION
105
+ Log.log.warn "Device #{mac}: Detected new Homie-version (#{mhash['$homie']})"\
106
+ + ' check hodmin for updates'
107
+ end
108
+ mhash.tap { |hs| hs.delete('$homie') }
109
+ # Some topics-names from homie do not fit our needs due to special chars like '/'.
110
+ # ['$fw/name','$fw/version','$fw/checksum']
111
+ # Replace '/' by '_':
112
+ mhash.each { |k, v| create_attr(k.to_s.delete('$').gsub(/\//, '_').tr('-', '_'), v) }
113
+
114
+ # mac only downcase and without separating ':'
115
+ @mac = mac.delete(':').downcase
116
+
117
+ # for selecting purposes we need some of our topics in different varnames:
118
+ @checksum = @fw_checksum
119
+ # do we find a higher version of this firmware than installed one?
120
+ @upgradable = fw_list.empty? ? false : upgradable?(fw_name, fw_version, fw_list)
121
+ Log.log.info "Homie-Device detected: mac=#{@mac}, #{online_status}, " \
122
+ + " running #{fw_name}, #{fw_version}, upgr=#{upgradable}"
123
+ end
124
+
125
+ # Helper to create instance variables on the fly:
126
+ def create_method(name, &block)
127
+ self.class.send(:define_method, name, &block)
128
+ end
129
+
130
+ # Helper to create instance variables on the fly:
131
+ def create_attr(name, value)
132
+ create_method(name.to_sym) { instance_variable_get('@' + name) }
133
+ instance_variable_set('@' + name, value)
134
+ end
135
+
136
+ # Helper to determine status of a device (online/offline). Checks it via reading the online-topic
137
+ # of device at time of creating this object.
138
+ # WARNING: If you use this in a longer running program, this info may be outdated.
139
+ # In this case you should create a method that establishes a connection to your broker and
140
+ # reads the online-topic of this device during execution time.
141
+ def online?
142
+ online.casecmp('true').zero?
143
+ end
144
+
145
+ # Helper to create a string containing ONLINE or OFFLINE. Gets it via reading the online-topic
146
+ # of the device at time of creating this object.
147
+ # WARNING: If you use this in a longer running program, this info may be outdated.
148
+ # In this case you should create a method that establishes a connection to your broker and
149
+ # reads the online-topic of this device during execution time.
150
+ def online_status
151
+ online? ? 'ONLINE' : 'OFFLINE'
152
+ end
153
+
154
+ # Helper to push a firmware-file vai MQTT to our Homie-Device.
155
+ def push_firmware_to_dev(new_firmware)
156
+ bin_file = File.read(new_firmware.file_path)
157
+ md5_bin_file = Digest::MD5.hexdigest(bin_file)
158
+ base_topic = configatron.mqtt.base_topic + mac + '/'
159
+ client = mqtt_connect
160
+ sended = FALSE
161
+ client.publish(base_topic + '$implementation/ota/checksum', md5_bin_file, retain = false)
162
+ sleep 0.1
163
+ client.subscribe(base_topic + '$implementation/ota/status')
164
+ cursor = TTY::Cursor
165
+ puts ' '
166
+ client.get do |_topic, message|
167
+ ms = message
168
+ ms = message.split(/ /).first.strip if message.include?(' ')
169
+ if ms == '206'
170
+ now, ges = message.split(/ /).last.strip.split(/\//).map(&:to_i)
171
+ actual = (now / ges.to_f * 100).round(0)
172
+ print cursor.column(1)
173
+ print "Pushing firmware, #{actual}% done"
174
+ end
175
+ if ms == '304'
176
+ puts '304, file already installed. No action needed. ' + message
177
+ break
178
+ end
179
+ if ms == '403'
180
+ puts '403, OTA disabled:' + message
181
+ break
182
+ end
183
+ if ms == '400'
184
+ puts '400, Bad checksum:' + message
185
+ break
186
+ end
187
+ if ms == '202'
188
+ puts '202, pushing file'
189
+ client.publish(base_topic + '$implementation/ota/firmware', bin_file, retain = false)
190
+ sended = TRUE
191
+ end
192
+ if ms == '200' && sended
193
+ puts "\nFile-md5=#{md5_bin_file} installed, device #{name} is rebooting"
194
+ break
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ # Class represents a pair of a Homie-Device and a firmware running on this device
201
+ class HomiePair
202
+ attr_reader :hdev, :hfw
203
+ def initialize(dev, *fw)
204
+ fw.flatten!
205
+ @hdev = dev.nil? ? nil : dev
206
+ @hfw = fw.empty? ? nil : fw.first
207
+ end
208
+ end
209
+
210
+ # Reads all Homie-Devices from given broker.
211
+ # To be called with connected MQTT-client. Topic has to be set in calling program.
212
+ # Variable timeout_seconds defines, after what time out client.get will be cancelled. Choose
213
+ # a value high enough for your data, but fast enough for quick response. default is 0.7 sec,
214
+ # which should be enough for a lot of Homies controlled by a broker running on a Raspberry-PI.
215
+ def get_homies(client, *fw_list)
216
+ allmqtt = []
217
+ begin
218
+ Timeout.timeout(configatron.mqtt.timeout.to_f) do
219
+ client.get { |topic, message| allmqtt << [topic, message] }
220
+ end
221
+ # we want to read all published messages right now and then leave (otherwise we are blocked)
222
+ rescue Timeout::Error
223
+ end
224
+ # find all homie-IDs (MAC-addresses)
225
+ macs = allmqtt.select { |t, _m| t.include?('/$homie') }.map { |t, _m| t.split('/$').first.split('/').last }
226
+ # create a array of homie-devices for macs in our list:
227
+ homies = []
228
+ macs.each do |mac|
229
+ mqtt = allmqtt.select { |t, _m| t.include?(mac) }
230
+ homies << HomieDevice.new(mqtt, fw_list)
231
+ end
232
+ homies
233
+ end
234
+
235
+ # Return a list of Homie-Devices controlled by given broker.
236
+ def fetch_homie_dev_list(*fw_list)
237
+ client = mqtt_connect
238
+ base_topic = configatron.mqtt.base_topic + '#'
239
+ client.subscribe(base_topic)
240
+ list = get_homies(client, fw_list)
241
+ client.disconnect
242
+ list
243
+ end
244
+
245
+ # Return a list of Homie-firmwares found in given diretory-tree. Firmwares are identfied
246
+ # by Magic-byte (see https://homie-esp8266.readme.io/v2.0.0/docs/magic-bytes). Filenames
247
+ # are ignored, you can specify a pattern in hodmin-config to speed up searching.
248
+ # Default filename-pattern is '*.bin'
249
+ def fetch_homie_fw_list
250
+ directory = configatron.firmware.dir + '**/' + configatron.firmware.filepattern
251
+ Log.log.info "Scanning dir: #{directory}"
252
+ binlist = Dir[directory]
253
+ fw_list = []
254
+ binlist.each do |fw|
255
+ fw_list << FirmwareHomie.new(fw) if homie_firmware?(fw)
256
+ end
257
+ fw_list
258
+ end
259
+
260
+ # Extends Array class with some specific selection-methods for devices and firmware
261
+ class Array
262
+ # Selects Array of HomieDevices or firmwares based on options
263
+ def select_by_opts(options)
264
+ this_object = first.class == HomieDevice ? 'HD' : 'FW'
265
+
266
+ # Options valid for selecting Homie-Devices OR for firmwares
267
+ valid_dev_options =
268
+ this_object == 'HD' ? [:mac, :fw_name, :checksum, :localip] : [:checksum, :fw_name, :config]
269
+
270
+ # use only valid options:
271
+ my_opts = options.select { |k, _v| valid_dev_options.include?(k) }
272
+
273
+ # remove all options not used as CLI argument:
274
+ my_opts = my_opts.select { |_k, v| !v.to_s.empty? }
275
+ return self if my_opts.empty? # no options set, so all devices are selected
276
+
277
+ my_devs = self
278
+ # selects objects (devices or firmwares) from an array due to a filter defined by key-value-pair
279
+ # Example: [:checksum => 'c79*']
280
+ my_opts.each_pair do |k, v|
281
+ # puts "looking for #{k} = #{v}"
282
+ my_devs = my_devs.select { |h| SelectObject.new(v) =~ h.instance_variable_get("@#{k}") }
283
+ end
284
+ my_devs
285
+ end
286
+
287
+ # Creates an array of rows with desired output from HomiePair-objects.
288
+ def create_output_table(attribs, _style)
289
+ pastel = Pastel.new
290
+ empty_field = ' '
291
+ empty_field = configatron.output.nil.strip unless configatron.output.nil? || configatron.output.nil.nil?
292
+ empty_field = pastel.dim(empty_field) # dim this message
293
+ rows = []
294
+ each do |r|
295
+ row = []
296
+ # color for checksum if applicable:
297
+ checksum_color = if r.hdev.nil?
298
+ 'none'
299
+ else
300
+ r.hdev.upgradable ? 'yellow' : 'green'
301
+ end
302
+ attribs.each do |a|
303
+ row << case a.slice(0, 2)
304
+ when 'HD' then
305
+ var_name = a.gsub(/HD./, '')
306
+ var = r.hdev.nil? ? empty_field : r.hdev.instance_variable_get("@#{a.gsub(/HD./, '')}")
307
+ case var_name
308
+ when 'online'
309
+ var = pastel.green(var) if var == 'true'
310
+ var = pastel.red(var) if var == 'false'
311
+ var if var != 'true' && var != 'false'
312
+ else
313
+ var = var.nil? ? empty_field : var
314
+ end
315
+ when 'FW' then
316
+ if r.hfw.nil?
317
+ empty_field
318
+ else
319
+ var_name = a.gsub(/FW./, '')
320
+ var = r.hfw.instance_variable_get("@#{var_name}")
321
+ case var_name
322
+ when 'checksum'
323
+ case checksum_color
324
+ when 'none' then var
325
+ when 'green' then pastel.green(var)
326
+ when 'yellow' then pastel.yellow(var)
327
+ end
328
+ else
329
+ var.nil? ? empty_field : var
330
+ end
331
+ end
332
+ when 'AD' then
333
+ var = r.instance_variable_get("@#{a.gsub(/AD./, '')}")
334
+ var.nil? ? empty_field : var
335
+ end
336
+ end
337
+ rows << row
338
+ end
339
+ rows
340
+ end
341
+ end
342
+
343
+ # Check a firmware against available bin-files of Homie-firmwares.
344
+ # Returns true, if there is a higher Version than installed.
345
+ # Returns false, if there is no suitable firmware-file found or installed version is the
346
+ # highest version found.
347
+ def upgradable?(fw_name, fw_version, fw_list)
348
+ fw_list.flatten!
349
+ # select highest Version of fw_name from given firmware_list:
350
+ return false if fw_list.empty? # No entries in Softwarelist
351
+ best_version = fw_list.select { |h| h.fw_name == fw_name }\
352
+ .sort_by(&:fw_version).last
353
+ best_version.nil? ? false : fw_version < best_version.fw_version
354
+ end
355
+
356
+ # Special class to select a string via Regex. Needed for flexible search for MAC,
357
+ # firmware-name and so on. Helper to construct a Regex.
358
+ class SelectObject
359
+ def self.string_to_regex(var)
360
+ Regexp.new "^#{Regexp.escape(var).gsub('\*', '.*?')}$"
361
+ end
362
+
363
+ # Helper
364
+ def initialize(var)
365
+ @regex = self.class.string_to_regex(var)
366
+ end
367
+
368
+ # Helper
369
+ def =~(other)
370
+ !!(other =~ @regex)
371
+ end
372
+ end
373
+
374
+ # Extends String-class with the ability to check whether JSON-items with Hodmin-CFG-data
375
+ # within self-string includes given string. Compares two strings as Hashes: if key/values
376
+ # from str are included in self: TRUE, otherwise FALSE
377
+ class String
378
+ def include_cfg?(str)
379
+ return false if self == '' || str == ''
380
+ h1 = JSON.parse(self)
381
+ h2 = JSON.parse(str)
382
+ (h2.to_a - h1.to_a).empty?
383
+ end
384
+ end
385
+
386
+ # Checks Hash with config-data
387
+ def check_config_ok?(config, configfile)
388
+ status_ok = true
389
+ if config['mqtt']['host'] == 'mqtt.example.com'
390
+ puts "ERR: No valid config-file found.\nPlease edit config file: #{configfile}."
391
+ status_ok = false
392
+ end
393
+
394
+ if !config['mqtt']['base_topic'].empty? && config['mqtt']['base_topic'].split(//).last != '/'
395
+ puts "ERR: mqtt: base_topic MUST end with '/'. Base_topic given: #{config['mqtt']['base_topic']}"
396
+ status_ok = false
397
+ end
398
+ status_ok
399
+ end
400
+
401
+ # Returns Hash with default config-data for Hodmin. File MUST be edited after creation by user.
402
+ def default_config
403
+ config = {}
404
+ config['mqtt'] = Hash['protocol' => 'mqtt://', 'host' => 'mqtt.example.com', 'port' => '1883',\
405
+ 'user' => 'username', 'password' => 'password', 'base_topic' => 'devices/homie/',\
406
+ 'auth' => true, 'timeout' => 0.3]
407
+ config['firmware'] = Hash['dir' => '/home/user/sketchbook/', 'filepattern' => '*.bin']
408
+ config['logging'] = Hash['logdestination' => 'nil']
409
+ config['output'] = Hash['list' => 'HD.mac HD.online HD.localip HD.name FW.checksum'\
410
+ + 'FW.fw_name FW.fw_version HD.upgradable', 'nil' => '']
411
+ config
412
+ end
413
+
414
+ # Returns Hash with default config-data to initialize a Homie-device. File MUST be edited after creation by user.
415
+ def default_config_initialize
416
+ config = {}
417
+ config['name'] = 'Homie1234'
418
+ config['wifi'] = Hash['ssid' => 'myWifi', 'password'=>'password']
419
+ config['mqtt'] = Hash['host' => 'myhost.mydomain.local', 'port' => 1883, 'base_topic'=>'devices/homie/'\
420
+ , 'auth'=>true, 'username'=>'user1', 'password' => 'mqttpassword']
421
+ config['ota'] = Hash['enabled' => true]
422
+ config
423
+ end
@@ -0,0 +1,3 @@
1
+ module Hodmin
2
+ VERSION = "0.1.2"
3
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hodmin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Thomas Romeyke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: configatron
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.5'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 4.5.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '4.5'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 4.5.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: mqtt
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.4.0
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.4.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: pastel
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.7.1
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.7.1
61
+ - !ruby/object:Gem::Dependency
62
+ name: trollop
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.1'
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 2.1.2
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '2.1'
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 2.1.2
81
+ - !ruby/object:Gem::Dependency
82
+ name: tty-cursor
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 0.4.0
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 0.4.0
95
+ - !ruby/object:Gem::Dependency
96
+ name: tty-table
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 0.7.0
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 0.7.0
109
+ - !ruby/object:Gem::Dependency
110
+ name: bundler
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '1.14'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '1.14'
123
+ - !ruby/object:Gem::Dependency
124
+ name: rake
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '10.0'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '10.0'
137
+ - !ruby/object:Gem::Dependency
138
+ name: minitest
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '5.0'
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '5.0'
151
+ description: Hodmin enables you to administrate your Homie-devices via command-line-interface
152
+ (CLI). It consists of some scripts to wrap homie-administration in some handy commands.
153
+ Hodmin does not communicate with a homie-device directly. It instead uses your MQTT-broker
154
+ to pass informations to a device.
155
+ email:
156
+ - rttools@googlemail.com
157
+ executables:
158
+ - hodmin
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - bin/hodmin
163
+ - lib/hodmin.rb
164
+ - lib/hodmin/hodmin_initialize.rb
165
+ - lib/hodmin/hodmin_list.rb
166
+ - lib/hodmin/hodmin_pull_config.rb
167
+ - lib/hodmin/hodmin_push_config.rb
168
+ - lib/hodmin/hodmin_push_firmware.rb
169
+ - lib/hodmin/hodmin_remove.rb
170
+ - lib/hodmin/hodmin_rename.rb
171
+ - lib/hodmin/hodmin_tools.rb
172
+ - lib/hodmin/version.rb
173
+ homepage: http://www.github.com/rttools/hodmin
174
+ licenses:
175
+ - GPL-3.0
176
+ metadata:
177
+ allowed_push_host: https://rubygems.org
178
+ post_install_message:
179
+ rdoc_options: []
180
+ require_paths:
181
+ - lib
182
+ required_ruby_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ requirements: []
193
+ rubyforge_project:
194
+ rubygems_version: 2.5.1
195
+ signing_key:
196
+ specification_version: 4
197
+ summary: Hodmin is a tool to administrate Homie-devices (esp8266-based microcomputers
198
+ with Homie-firmware)
199
+ test_files: []