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.
- 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: []
|