hodmin 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []