hodmin 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/hodmin +159 -0
- data/lib/hodmin.rb +5 -0
- data/lib/hodmin/hodmin_initialize.rb +42 -0
- data/lib/hodmin/hodmin_list.rb +62 -0
- data/lib/hodmin/hodmin_pull_config.rb +42 -0
- data/lib/hodmin/hodmin_push_config.rb +96 -0
- data/lib/hodmin/hodmin_push_firmware.rb +62 -0
- data/lib/hodmin/hodmin_remove.rb +32 -0
- data/lib/hodmin/hodmin_rename.rb +36 -0
- data/lib/hodmin/hodmin_tools.rb +423 -0
- data/lib/hodmin/version.rb +3 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/hodmin
ADDED
@@ -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
|
data/lib/hodmin.rb
ADDED
@@ -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
|
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: []
|