rbzk 0.1.0 → 0.1.1
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 +4 -4
- data/README.md +161 -2
- data/bin/console +14 -0
- data/bin/rbzk +8 -0
- data/bin/setup +6 -0
- data/lib/rbzk/cli/commands.rb +551 -0
- data/lib/rbzk/cli/config.rb +85 -0
- data/lib/rbzk/cli_thor.rb +14 -0
- data/lib/rbzk/constants.rb +0 -1
- data/lib/rbzk/zk.rb +56 -635
- metadata +35 -14
@@ -0,0 +1,551 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
require 'yaml'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'date'
|
8
|
+
require 'rbzk'
|
9
|
+
require 'rbzk/cli/config'
|
10
|
+
require 'terminal-table'
|
11
|
+
|
12
|
+
module RBZK
|
13
|
+
module CLI
|
14
|
+
class Commands < Thor
|
15
|
+
|
16
|
+
# Global options
|
17
|
+
class_option :ip, type: :string, desc: 'IP address of the device'
|
18
|
+
class_option :port, type: :numeric, desc: 'Port number (default: 4370)'
|
19
|
+
class_option :timeout, type: :numeric, desc: 'Connection timeout in seconds (default: 30)'
|
20
|
+
class_option :password, type: :numeric, desc: 'Device password (default: 0)'
|
21
|
+
class_option :verbose, type: :boolean, desc: 'Enable verbose output'
|
22
|
+
class_option :force_udp, type: :boolean, desc: 'Force UDP mode'
|
23
|
+
class_option :no_ping, type: :boolean, desc: 'Skip ping check'
|
24
|
+
class_option :encoding, type: :string, desc: 'Encoding for strings (default: UTF-8)'
|
25
|
+
|
26
|
+
desc "info [IP]", "Get device information"
|
27
|
+
|
28
|
+
def info(ip = nil)
|
29
|
+
|
30
|
+
# Use IP from options if not provided as argument
|
31
|
+
ip ||= options[:ip] || @config['ip']
|
32
|
+
|
33
|
+
with_connection(ip, options) do |conn|
|
34
|
+
# Get device information
|
35
|
+
# First read sizes to get user counts and capacities
|
36
|
+
conn.read_sizes
|
37
|
+
|
38
|
+
device_info = {
|
39
|
+
'Serial Number' => conn.get_serialnumber,
|
40
|
+
'MAC Address' => conn.get_mac,
|
41
|
+
'Device Name' => conn.get_device_name,
|
42
|
+
'Firmware Version' => conn.get_firmware_version,
|
43
|
+
'Platform' => conn.get_platform,
|
44
|
+
'Face Version' => conn.get_face_version,
|
45
|
+
'Fingerprint Version' => conn.get_fp_version,
|
46
|
+
'Device Time' => conn.get_time,
|
47
|
+
'Users' => conn.instance_variable_get(:@users),
|
48
|
+
'Fingerprints' => conn.instance_variable_get(:@fingers),
|
49
|
+
'Attendance Records' => conn.instance_variable_get(:@records),
|
50
|
+
'User Capacity' => conn.instance_variable_get(:@users_cap),
|
51
|
+
'Fingerprint Capacity' => conn.instance_variable_get(:@fingers_cap),
|
52
|
+
'Record Capacity' => conn.instance_variable_get(:@rec_cap),
|
53
|
+
'Face Capacity' => conn.instance_variable_get(:@faces_cap),
|
54
|
+
'Faces' => conn.instance_variable_get(:@faces)
|
55
|
+
}
|
56
|
+
|
57
|
+
# Display information
|
58
|
+
if defined?(::Terminal) && defined?(::Terminal::Table)
|
59
|
+
# Pretty table output
|
60
|
+
table = ::Terminal::Table.new do |t|
|
61
|
+
t.title = "Device Information"
|
62
|
+
device_info.each do |key, value|
|
63
|
+
t << [ key, value ]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
puts table
|
68
|
+
else
|
69
|
+
# Fallback plain text output
|
70
|
+
puts "Device Information:"
|
71
|
+
device_info.each do |key, value|
|
72
|
+
puts "#{key}: #{value}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
desc "users [IP]", "Get users from the device"
|
79
|
+
|
80
|
+
def users(ip = nil)
|
81
|
+
# Use IP from options if not provided as argument
|
82
|
+
ip ||= options[:ip] || @config['ip']
|
83
|
+
with_connection(ip, options) do |conn|
|
84
|
+
puts "Getting users..."
|
85
|
+
users = conn.get_users
|
86
|
+
display_users(users)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
desc "logs [IP]", "Get attendance logs"
|
91
|
+
method_option :today, type: :boolean, desc: "Get only today's logs"
|
92
|
+
method_option :yesterday, type: :boolean, desc: "Get only yesterday's logs"
|
93
|
+
method_option :week, type: :boolean, desc: "Get this week's logs"
|
94
|
+
method_option :month, type: :boolean, desc: "Get this month's logs"
|
95
|
+
method_option :start_date, type: :string, desc: "Start date for custom range (YYYY-MM-DD)"
|
96
|
+
method_option :end_date, type: :string, desc: "End date for custom range (YYYY-MM-DD)"
|
97
|
+
method_option :limit, type: :numeric, default: 25, desc: "Limit the number of logs displayed (default: 25, use 0 for all)"
|
98
|
+
|
99
|
+
# Add aliases for common log commands
|
100
|
+
desc "logs-today [IP]", "Get today's attendance logs"
|
101
|
+
map "logs-today" => "logs"
|
102
|
+
|
103
|
+
def logs_today(ip = nil)
|
104
|
+
|
105
|
+
invoke :logs, [ ip ], today: true
|
106
|
+
end
|
107
|
+
|
108
|
+
desc "logs-yesterday [IP]", "Get yesterday's attendance logs"
|
109
|
+
map "logs-yesterday" => "logs"
|
110
|
+
|
111
|
+
def logs_yesterday(ip = nil)
|
112
|
+
invoke :logs, [ ip ], yesterday: true
|
113
|
+
end
|
114
|
+
|
115
|
+
desc "logs-week [IP]", "Get this week's attendance logs"
|
116
|
+
map "logs-week" => "logs"
|
117
|
+
|
118
|
+
def logs_week(ip = nil)
|
119
|
+
invoke :logs, [ ip ], week: true
|
120
|
+
end
|
121
|
+
|
122
|
+
desc "logs-month [IP]", "Get this month's attendance logs"
|
123
|
+
map "logs-month" => "logs"
|
124
|
+
|
125
|
+
def logs_month(ip = nil)
|
126
|
+
invoke :logs, [ ip ], month: true
|
127
|
+
end
|
128
|
+
|
129
|
+
desc "logs-all [IP]", "Get all attendance logs without limit"
|
130
|
+
|
131
|
+
def logs_all(ip = nil)
|
132
|
+
# Use IP from options if not provided as argument
|
133
|
+
ip ||= options[:ip] || @config['ip']
|
134
|
+
|
135
|
+
with_connection(ip, options) do |conn|
|
136
|
+
# Get attendance logs
|
137
|
+
puts "Getting all attendance logs (this may take a while)..."
|
138
|
+
logs = conn.get_attendance_logs
|
139
|
+
total_logs = logs.size
|
140
|
+
puts "Total logs: #{total_logs}" if options[:verbose]
|
141
|
+
|
142
|
+
# Display logs
|
143
|
+
if logs && !logs.empty?
|
144
|
+
puts "\nFound #{logs.size} attendance records:"
|
145
|
+
|
146
|
+
if defined?(::Terminal) && defined?(::Terminal::Table)
|
147
|
+
# Pretty table output
|
148
|
+
table = ::Terminal::Table.new do |t|
|
149
|
+
t.title = "All Attendance Logs (Showing All Records)"
|
150
|
+
t.headings = [ 'User ID', 'Time', 'Status' ]
|
151
|
+
|
152
|
+
# Show all logs in the table
|
153
|
+
logs.each do |log|
|
154
|
+
t << [
|
155
|
+
log.user_id,
|
156
|
+
log.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
157
|
+
format_status(log.status)
|
158
|
+
]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
puts table
|
163
|
+
else
|
164
|
+
# Fallback plain text output
|
165
|
+
logs.each do |log|
|
166
|
+
puts " User ID: #{log.user_id}, Time: #{log.timestamp.strftime('%Y-%m-%d %H:%M:%S')}, Status: #{format_status(log.status)}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
else
|
170
|
+
puts "\nNo attendance records found"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
desc "logs-custom START_DATE END_DATE [IP]", "Get logs for a custom date range (YYYY-MM-DD)"
|
176
|
+
|
177
|
+
def logs_custom(start_date, end_date, ip = nil)
|
178
|
+
# Use IP from options if not provided as argument
|
179
|
+
ip ||= options[:ip] || @config['ip']
|
180
|
+
invoke :logs, [ ip ], start_date: start_date, end_date: end_date
|
181
|
+
end
|
182
|
+
|
183
|
+
desc "logs [IP]", "Get all attendance logs"
|
184
|
+
|
185
|
+
def logs(ip = nil)
|
186
|
+
# Use IP from options if not provided as argument
|
187
|
+
ip ||= options[:ip] || @config['ip']
|
188
|
+
with_connection(ip, options) do |conn|
|
189
|
+
# Get attendance logs
|
190
|
+
puts "Getting attendance logs..."
|
191
|
+
logs = conn.get_attendance_logs
|
192
|
+
total_logs = logs.size
|
193
|
+
puts "Total logs: #{total_logs}" if options[:verbose]
|
194
|
+
|
195
|
+
# Filter logs based on options
|
196
|
+
title = if options[:today]
|
197
|
+
today = Date.today
|
198
|
+
logs = filter_logs_by_date(logs, today, today)
|
199
|
+
"Today's Attendance Logs (#{today})"
|
200
|
+
elsif options[:yesterday]
|
201
|
+
yesterday = Date.today - 1
|
202
|
+
logs = filter_logs_by_date(logs, yesterday, yesterday)
|
203
|
+
"Yesterday's Attendance Logs (#{yesterday})"
|
204
|
+
elsif options[:week]
|
205
|
+
today = Date.today
|
206
|
+
start_of_week = today - today.wday
|
207
|
+
logs = filter_logs_by_date(logs, start_of_week, today)
|
208
|
+
"This Week's Attendance Logs (#{start_of_week} to #{today})"
|
209
|
+
elsif options[:month]
|
210
|
+
today = Date.today
|
211
|
+
start_of_month = Date.new(today.year, today.month, 1)
|
212
|
+
logs = filter_logs_by_date(logs, start_of_month, today)
|
213
|
+
"This Month's Attendance Logs (#{start_of_month} to #{today})"
|
214
|
+
elsif options[:start_date] && options[:end_date]
|
215
|
+
begin
|
216
|
+
start_date = Date.parse(options[:start_date])
|
217
|
+
end_date = Date.parse(options[:end_date])
|
218
|
+
|
219
|
+
# Print debug info
|
220
|
+
puts "Filtering logs from #{start_date} to #{end_date}..." if options[:verbose]
|
221
|
+
|
222
|
+
# Use the filter_logs_by_date method
|
223
|
+
logs = filter_logs_by_date(logs, start_date, end_date)
|
224
|
+
|
225
|
+
"Attendance Logs (#{start_date} to #{end_date})"
|
226
|
+
rescue ArgumentError
|
227
|
+
puts "Error: Invalid date format. Please use YYYY-MM-DD format."
|
228
|
+
return
|
229
|
+
end
|
230
|
+
elsif options[:start_date]
|
231
|
+
begin
|
232
|
+
start_date = Date.parse(options[:start_date])
|
233
|
+
end_date = Date.today
|
234
|
+
|
235
|
+
# Print debug info
|
236
|
+
puts "Filtering logs from #{start_date} onwards..." if options[:verbose]
|
237
|
+
|
238
|
+
# Use the filter_logs_by_date method
|
239
|
+
logs = filter_logs_by_date(logs, start_date, end_date)
|
240
|
+
|
241
|
+
"Attendance Logs (#{start_date} to #{end_date})"
|
242
|
+
rescue ArgumentError
|
243
|
+
puts "Error: Invalid date format. Please use YYYY-MM-DD format."
|
244
|
+
return
|
245
|
+
end
|
246
|
+
elsif options[:end_date]
|
247
|
+
begin
|
248
|
+
end_date = Date.parse(options[:end_date])
|
249
|
+
# Default start date to 30 days before end date
|
250
|
+
start_date = end_date - 30
|
251
|
+
|
252
|
+
# Print debug info
|
253
|
+
puts "Filtering logs from #{start_date} to #{end_date}..." if options[:verbose]
|
254
|
+
|
255
|
+
# Use the filter_logs_by_date method
|
256
|
+
logs = filter_logs_by_date(logs, start_date, end_date)
|
257
|
+
|
258
|
+
"Attendance Logs (#{start_date} to #{end_date})"
|
259
|
+
rescue ArgumentError
|
260
|
+
puts "Error: Invalid date format. Please use YYYY-MM-DD format."
|
261
|
+
return
|
262
|
+
end
|
263
|
+
else
|
264
|
+
"All Attendance Logs"
|
265
|
+
end
|
266
|
+
|
267
|
+
# Display logs
|
268
|
+
if logs && !logs.empty?
|
269
|
+
puts "\nFound #{logs.size} attendance records:"
|
270
|
+
|
271
|
+
# Determine how many logs to display
|
272
|
+
limit = options[:limit] || 25
|
273
|
+
display_logs = limit > 0 ? logs.first(limit) : logs
|
274
|
+
|
275
|
+
if defined?(::Terminal) && defined?(::Terminal::Table)
|
276
|
+
# Pretty table output
|
277
|
+
table = ::Terminal::Table.new do |t|
|
278
|
+
t.title = title || "Attendance Logs"
|
279
|
+
t.headings = [ 'User ID', 'Time', 'Status' ]
|
280
|
+
|
281
|
+
# Show logs in the table based on limit
|
282
|
+
display_logs.each do |log|
|
283
|
+
t << [
|
284
|
+
log.user_id,
|
285
|
+
log.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
286
|
+
format_status(log.status)
|
287
|
+
]
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
puts table
|
292
|
+
|
293
|
+
# Show summary if logs were limited
|
294
|
+
if limit > 0 && logs.size > limit
|
295
|
+
puts "Showing #{display_logs.size} of #{logs.size} records. Use --limit option to change the number of records displayed."
|
296
|
+
end
|
297
|
+
else
|
298
|
+
# Fallback plain text output
|
299
|
+
display_logs.each do |log|
|
300
|
+
puts " User ID: #{log.user_id}, Time: #{log.timestamp.strftime('%Y-%m-%d %H:%M:%S')}, Status: #{format_status(log.status)}"
|
301
|
+
end
|
302
|
+
|
303
|
+
if logs.size > display_logs.size
|
304
|
+
puts " ... and #{logs.size - display_logs.size} more records"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
else
|
308
|
+
puts "\nNo attendance records found"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
desc "clear-logs [IP]", "Clear attendance logs"
|
314
|
+
map "clear-logs" => :clear_logs
|
315
|
+
|
316
|
+
def clear_logs(ip = nil)
|
317
|
+
|
318
|
+
# Use IP from options if not provided as argument
|
319
|
+
ip ||= options[:ip] || @config['ip']
|
320
|
+
with_connection(ip, options) do |conn|
|
321
|
+
puts "WARNING: This will delete all attendance logs from the device."
|
322
|
+
return unless yes?("Are you sure you want to continue? (y/N)")
|
323
|
+
|
324
|
+
puts "Clearing attendance logs..."
|
325
|
+
result = conn.clear_attendance
|
326
|
+
puts "✓ Attendance logs cleared successfully!" if result
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
desc "test-voice [IP]", "Test the device voice"
|
331
|
+
method_option :index, type: :numeric, desc: "Sound index to play (0-35, default: 0)"
|
332
|
+
map "test-voice" => :test_voice
|
333
|
+
|
334
|
+
def test_voice(ip = nil)
|
335
|
+
# Use IP from options if not provided as argument
|
336
|
+
ip ||= options[:ip] || @config['ip']
|
337
|
+
|
338
|
+
# Get the sound index
|
339
|
+
index = options[:index] || 0
|
340
|
+
|
341
|
+
# Print available sound indices if verbose
|
342
|
+
if options[:verbose]
|
343
|
+
puts "Available sound indices:"
|
344
|
+
puts " 0: Thank You"
|
345
|
+
puts " 1: Incorrect Password"
|
346
|
+
puts " 2: Access Denied"
|
347
|
+
puts " 3: Invalid ID"
|
348
|
+
puts " 4: Please try again"
|
349
|
+
puts " 5: Duplicate ID"
|
350
|
+
puts " 6: The clock is flow"
|
351
|
+
puts " 7: The clock is full"
|
352
|
+
puts " 8: Duplicate finger"
|
353
|
+
puts " 9: Duplicated punch"
|
354
|
+
puts "10: Beep kuko"
|
355
|
+
puts "11: Beep siren"
|
356
|
+
puts "13: Beep bell"
|
357
|
+
puts "18: Windows(R) opening sound"
|
358
|
+
puts "20: Fingerprint not emolt"
|
359
|
+
puts "21: Password not emolt"
|
360
|
+
puts "22: Badges not emolt"
|
361
|
+
puts "23: Face not emolt"
|
362
|
+
puts "24: Beep standard"
|
363
|
+
puts "30: Invalid user"
|
364
|
+
puts "31: Invalid time period"
|
365
|
+
puts "32: Invalid combination"
|
366
|
+
puts "33: Illegal Access"
|
367
|
+
puts "34: Disk space full"
|
368
|
+
puts "35: Duplicate fingerprint"
|
369
|
+
puts "51: Focus eyes on the green box"
|
370
|
+
end
|
371
|
+
|
372
|
+
with_connection(ip, options) do |conn|
|
373
|
+
puts "Testing device voice with index #{index}..."
|
374
|
+
result = conn.test_voice(index)
|
375
|
+
puts "✓ Voice test successful!" if result
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
desc "config", "Show current configuration"
|
380
|
+
|
381
|
+
def config
|
382
|
+
puts "RBZK Configuration"
|
383
|
+
puts "=================="
|
384
|
+
@config.to_h.each do |key, value|
|
385
|
+
puts "#{key}: #{value}"
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
desc "config-set KEY VALUE", "Set a configuration value"
|
390
|
+
|
391
|
+
def config_set(key, value)
|
392
|
+
# Convert value to appropriate type
|
393
|
+
typed_value = case key
|
394
|
+
when 'port', 'timeout', 'password'
|
395
|
+
value.to_i
|
396
|
+
when 'verbose', 'force_udp', 'no_ping'
|
397
|
+
value.downcase == 'true'
|
398
|
+
else
|
399
|
+
value
|
400
|
+
end
|
401
|
+
|
402
|
+
@config[key] = typed_value
|
403
|
+
@config.save
|
404
|
+
puts "Configuration updated: #{key} = #{typed_value}"
|
405
|
+
end
|
406
|
+
|
407
|
+
desc "config-reset", "Reset configuration to defaults"
|
408
|
+
|
409
|
+
def config_reset
|
410
|
+
if yes?("Are you sure you want to reset all configuration to defaults? (y/N)")
|
411
|
+
FileUtils.rm_f(@config.config_file)
|
412
|
+
@config = RBZK::CLI::Config.new
|
413
|
+
puts "Configuration reset to defaults."
|
414
|
+
invoke :config
|
415
|
+
else
|
416
|
+
puts "Operation cancelled."
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
private
|
421
|
+
|
422
|
+
def initialize(*args)
|
423
|
+
super
|
424
|
+
@config = RBZK::CLI::Config.new
|
425
|
+
end
|
426
|
+
|
427
|
+
def with_connection(ip, options = {})
|
428
|
+
puts "Connecting to ZKTeco device at #{ip}:#{options[:port] || @config['port'] || 4370}..."
|
429
|
+
puts "Please ensure the device is powered on and connected to the network."
|
430
|
+
|
431
|
+
begin
|
432
|
+
# Create ZK instance with options from config and command line
|
433
|
+
zk_options = {
|
434
|
+
port: options[:port] || @config['port'] || 4370,
|
435
|
+
timeout: options[:timeout] || @config['timeout'] || 30,
|
436
|
+
password: options[:password] || @config['password'] || 0,
|
437
|
+
verbose: options[:verbose] || @config['verbose'] || false,
|
438
|
+
force_udp: options[:force_udp] || @config['force_udp'] || false,
|
439
|
+
omit_ping: options[:no_ping] || @config['no_ping'] || false,
|
440
|
+
encoding: options[:encoding] || @config['encoding'] || 'UTF-8'
|
441
|
+
}
|
442
|
+
|
443
|
+
zk = RBZK::ZK.new(ip, **zk_options)
|
444
|
+
conn = zk.connect
|
445
|
+
|
446
|
+
if conn.connected?
|
447
|
+
puts "✓ Connected successfully!" unless options[:quiet]
|
448
|
+
yield conn if block_given?
|
449
|
+
else
|
450
|
+
puts "✗ Failed to connect to device."
|
451
|
+
end
|
452
|
+
rescue RBZK::ZKNetworkError => e
|
453
|
+
puts "✗ Network Error: #{e.message}"
|
454
|
+
puts "Please check the IP address and ensure the device is reachable."
|
455
|
+
rescue RBZK::ZKErrorResponse => e
|
456
|
+
puts "✗ Device Error: #{e.message}"
|
457
|
+
puts "The device returned an error response."
|
458
|
+
rescue => e
|
459
|
+
puts "✗ Unexpected Error: #{e.message}"
|
460
|
+
puts "An unexpected error occurred while communicating with the device."
|
461
|
+
puts e.backtrace.join("\n") if options[:verbose]
|
462
|
+
ensure
|
463
|
+
if conn && conn.connected?
|
464
|
+
puts "Disconnecting from device..." unless options[:quiet]
|
465
|
+
conn.disconnect
|
466
|
+
puts "✓ Disconnected" unless options[:quiet]
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def display_users(users)
|
472
|
+
if users && !users.empty?
|
473
|
+
puts "✓ Found #{users.size} users:"
|
474
|
+
|
475
|
+
# Use Terminal::Table for pretty output
|
476
|
+
table = ::Terminal::Table.new do |t|
|
477
|
+
t.title = "Users"
|
478
|
+
t.headings = [ 'UID', 'User ID', 'Name', 'Privilege', 'Password', 'Group ID', 'Card' ]
|
479
|
+
|
480
|
+
users.each do |user|
|
481
|
+
t << [
|
482
|
+
user.uid,
|
483
|
+
user.user_id,
|
484
|
+
user.name,
|
485
|
+
format_privilege(user.privilege),
|
486
|
+
(user.password.nil? || user.password.empty?) ? '(none)' : '(set)',
|
487
|
+
user.group_id,
|
488
|
+
user.card
|
489
|
+
]
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
puts table
|
494
|
+
else
|
495
|
+
puts "✓ No users found"
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
def filter_logs_by_date(logs, start_date, end_date)
|
500
|
+
# Convert Date objects to strings for comparison
|
501
|
+
total_logs = logs.size
|
502
|
+
start_date_str = start_date.strftime('%Y-%m-%d')
|
503
|
+
end_date_str = end_date.strftime('%Y-%m-%d')
|
504
|
+
|
505
|
+
if options[:verbose]
|
506
|
+
puts "Filtering logs from #{start_date_str} to #{end_date_str}..."
|
507
|
+
puts "Total logs before filtering: #{total_logs}"
|
508
|
+
end
|
509
|
+
|
510
|
+
# Filter logs by date range using string comparison
|
511
|
+
filtered_logs = []
|
512
|
+
logs.each do |log|
|
513
|
+
log_date_str = log.timestamp.strftime('%Y-%m-%d')
|
514
|
+
if log_date_str >= start_date_str && log_date_str <= end_date_str
|
515
|
+
filtered_logs << log
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
if options[:verbose]
|
520
|
+
puts "Filtered logs: #{filtered_logs.size} of #{total_logs}"
|
521
|
+
end
|
522
|
+
|
523
|
+
# Return the filtered logs
|
524
|
+
filtered_logs
|
525
|
+
end
|
526
|
+
|
527
|
+
def format_status(status)
|
528
|
+
case status
|
529
|
+
when 0 then "Check In"
|
530
|
+
when 1 then "Check Out"
|
531
|
+
when 2 then "Break Out"
|
532
|
+
when 3 then "Break In"
|
533
|
+
when 4 then "Overtime In"
|
534
|
+
when 5 then "Overtime Out"
|
535
|
+
else "Unknown (#{status})"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
def format_privilege(privilege)
|
540
|
+
case privilege
|
541
|
+
when 0 then "User"
|
542
|
+
when 1 then "Enroller"
|
543
|
+
when 2 then "Manager"
|
544
|
+
when 3 then "Administrator"
|
545
|
+
when 14 then "Super Admin"
|
546
|
+
else "Unknown (#{privilege})"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module RBZK
|
5
|
+
module CLI
|
6
|
+
# Configuration handler for the CLI
|
7
|
+
class Config
|
8
|
+
# Default configuration values
|
9
|
+
DEFAULT_CONFIG = {
|
10
|
+
'ip' => '192.168.100.201',
|
11
|
+
'port' => 4370,
|
12
|
+
'timeout' => 30,
|
13
|
+
'password' => 0,
|
14
|
+
'verbose' => false,
|
15
|
+
'force_udp' => false,
|
16
|
+
'no_ping' => true,
|
17
|
+
'encoding' => 'UTF-8'
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
# Initialize a new configuration
|
21
|
+
# @param config_file [String] Path to the configuration file
|
22
|
+
def initialize(config_file = nil)
|
23
|
+
@config_file = config_file || default_config_file
|
24
|
+
@config = load_config
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get a configuration value
|
28
|
+
# @param key [String, Symbol] Configuration key
|
29
|
+
# @return [Object] Configuration value
|
30
|
+
def [](key)
|
31
|
+
@config[key.to_s]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set a configuration value
|
35
|
+
# @param key [String, Symbol] Configuration key
|
36
|
+
# @param value [Object] Configuration value
|
37
|
+
def []=(key, value)
|
38
|
+
@config[key.to_s] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
# Save the configuration to the file
|
42
|
+
def save
|
43
|
+
# Create the directory if it doesn't exist
|
44
|
+
FileUtils.mkdir_p(File.dirname(@config_file))
|
45
|
+
|
46
|
+
# Save the configuration
|
47
|
+
File.open(@config_file, 'w') do |f|
|
48
|
+
f.write(YAML.dump(@config))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get the default configuration file path
|
53
|
+
# @return [String] Default configuration file path
|
54
|
+
def default_config_file
|
55
|
+
if ENV['XDG_CONFIG_HOME']
|
56
|
+
File.join(ENV['XDG_CONFIG_HOME'], 'rbzk', 'config.yml')
|
57
|
+
elsif ENV['HOME']
|
58
|
+
File.join(ENV['HOME'], '.config', 'rbzk', 'config.yml')
|
59
|
+
else
|
60
|
+
File.join(Dir.pwd, '.rbzk.yml')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Load the configuration from the file
|
65
|
+
# @return [Hash] Configuration values
|
66
|
+
def load_config
|
67
|
+
if File.exist?(@config_file)
|
68
|
+
begin
|
69
|
+
config = YAML.load_file(@config_file)
|
70
|
+
return DEFAULT_CONFIG.merge(config) if config.is_a?(Hash)
|
71
|
+
rescue => e
|
72
|
+
warn "Error loading configuration file: #{e.message}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
DEFAULT_CONFIG.dup
|
76
|
+
end
|
77
|
+
|
78
|
+
# Get all configuration values
|
79
|
+
# @return [Hash] All configuration values
|
80
|
+
def to_h
|
81
|
+
@config.dup
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rbzk/cli/commands'
|
2
|
+
|
3
|
+
module RBZK
|
4
|
+
# Command Line Interface module for RBZK
|
5
|
+
# Provides methods for interacting with ZKTeco devices from the command line
|
6
|
+
module CLI
|
7
|
+
# Start the CLI with the given arguments
|
8
|
+
# @param args [Array<String>] Command line arguments
|
9
|
+
# @return [Integer] Exit code
|
10
|
+
def self.start(args = ARGV)
|
11
|
+
Commands.start(args)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/rbzk/constants.rb
CHANGED
@@ -48,7 +48,6 @@ module RBZK
|
|
48
48
|
CMD_DOORSTATE_RRQ = 75 # Obtain the door condition
|
49
49
|
CMD_WRITE_MIFARE = 76 # Write the Mifare card
|
50
50
|
CMD_EMPTY_MIFARE = 78 # Clear the Mifare card
|
51
|
-
CMD_PREPARE_BUFFER = 80 # Prepare buffer for data transfer
|
52
51
|
CMD_READFILE_DATA = 81 # Read data from buffer
|
53
52
|
CMD_GET_USERTEMP = 88 # Get an specific user template (uid, fid)
|
54
53
|
CMD_SAVE_USERTEMPS = 110 # Save user and multiple templates
|