keenetic 0.1.0
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/README.md +83 -0
- data/lib/keenetic/client.rb +361 -0
- data/lib/keenetic/configuration.rb +27 -0
- data/lib/keenetic/errors.rb +19 -0
- data/lib/keenetic/resources/base.rb +76 -0
- data/lib/keenetic/resources/devices.rb +309 -0
- data/lib/keenetic/resources/dhcp.rb +195 -0
- data/lib/keenetic/resources/internet.rb +169 -0
- data/lib/keenetic/resources/logs.rb +330 -0
- data/lib/keenetic/resources/network.rb +251 -0
- data/lib/keenetic/resources/policies.rb +171 -0
- data/lib/keenetic/resources/ports.rb +115 -0
- data/lib/keenetic/resources/routing.rb +200 -0
- data/lib/keenetic/resources/system.rb +267 -0
- data/lib/keenetic/resources/wifi.rb +376 -0
- data/lib/keenetic/version.rb +4 -0
- data/lib/keenetic.rb +99 -0
- metadata +89 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# System resource for accessing router system information and status.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading System Status
|
|
8
|
+
# GET /rci/show/system
|
|
9
|
+
# Returns: { cpuload, memtotal, memfree, membuffers, memcache, swaptotal, swapfree, uptime }
|
|
10
|
+
#
|
|
11
|
+
# === Reading System Info
|
|
12
|
+
# GET /rci/show/version
|
|
13
|
+
# Returns: { model, device, manufacturer, hw_version, release, ndm, ndw, ... }
|
|
14
|
+
#
|
|
15
|
+
# === Reading Defaults
|
|
16
|
+
# GET /rci/show/defaults
|
|
17
|
+
# Returns: Default configuration values (system-name, domain-name, etc.)
|
|
18
|
+
#
|
|
19
|
+
# === Reading License
|
|
20
|
+
# GET /rci/show/license
|
|
21
|
+
# Returns: { valid, active, expires, type, features, services }
|
|
22
|
+
#
|
|
23
|
+
class System < Base
|
|
24
|
+
# Get system resource usage (CPU, memory, swap).
|
|
25
|
+
#
|
|
26
|
+
# == Keenetic API Request
|
|
27
|
+
# GET /rci/show/system
|
|
28
|
+
#
|
|
29
|
+
# == Response Fields from API
|
|
30
|
+
# - cpuload: CPU usage percentage (0-100)
|
|
31
|
+
# - memtotal: Total RAM in bytes
|
|
32
|
+
# - memfree: Free RAM in bytes
|
|
33
|
+
# - membuffers: Buffer memory in bytes
|
|
34
|
+
# - memcache: Cached memory in bytes
|
|
35
|
+
# - swaptotal: Total swap in bytes
|
|
36
|
+
# - swapfree: Free swap in bytes
|
|
37
|
+
# - uptime: System uptime in seconds
|
|
38
|
+
#
|
|
39
|
+
# @return [Hash] Normalized resource data with :cpu, :memory, :swap, :uptime
|
|
40
|
+
# @example
|
|
41
|
+
# resources = client.system.resources
|
|
42
|
+
# # => { cpu: { load_percent: 15 },
|
|
43
|
+
# # memory: { total: 536870912, free: 268435456, used: 215789568, used_percent: 40.2 },
|
|
44
|
+
# # swap: { total: 0, free: 0, used: 0, used_percent: 0 },
|
|
45
|
+
# # uptime: 86400 }
|
|
46
|
+
#
|
|
47
|
+
def resources
|
|
48
|
+
response = get('/rci/show/system')
|
|
49
|
+
normalize_resources(response)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get router system information (model, firmware, hardware).
|
|
53
|
+
#
|
|
54
|
+
# == Keenetic API Request
|
|
55
|
+
# GET /rci/show/version
|
|
56
|
+
#
|
|
57
|
+
# == Response Fields from API
|
|
58
|
+
# - model: Router model name (e.g., "Keenetic Viva")
|
|
59
|
+
# - device: Device code (e.g., "KN-1912")
|
|
60
|
+
# - manufacturer: "Keenetic Ltd."
|
|
61
|
+
# - vendor: "Keenetic"
|
|
62
|
+
# - hw_version: Hardware revision
|
|
63
|
+
# - title: Firmware title
|
|
64
|
+
# - release: Firmware version string
|
|
65
|
+
# - ndm: { version, exact } - NDM version info
|
|
66
|
+
# - ndw: { version } - NDW version info
|
|
67
|
+
# - arch: CPU architecture (e.g., "mips", "aarch64")
|
|
68
|
+
# - components: Array of installed components
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash] Normalized system info
|
|
71
|
+
# @example
|
|
72
|
+
# info = client.system.info
|
|
73
|
+
# # => { model: "Keenetic Viva", device: "KN-1912", firmware_version: "4.01.C.7.0-0", ... }
|
|
74
|
+
#
|
|
75
|
+
def info
|
|
76
|
+
response = get('/rci/show/version')
|
|
77
|
+
normalize_info(response)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get system uptime in seconds.
|
|
81
|
+
#
|
|
82
|
+
# == Keenetic API Request
|
|
83
|
+
# GET /rci/show/system
|
|
84
|
+
#
|
|
85
|
+
# @return [Integer] Uptime in seconds
|
|
86
|
+
# @example
|
|
87
|
+
# client.system.uptime
|
|
88
|
+
# # => 86400 (1 day)
|
|
89
|
+
#
|
|
90
|
+
def uptime
|
|
91
|
+
response = get('/rci/show/system')
|
|
92
|
+
response['uptime'] if response.is_a?(Hash)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get default system configuration values.
|
|
96
|
+
#
|
|
97
|
+
# == Keenetic API Request
|
|
98
|
+
# GET /rci/show/defaults
|
|
99
|
+
#
|
|
100
|
+
# == Response Fields from API (examples)
|
|
101
|
+
# - system-name: Default router name
|
|
102
|
+
# - domain-name: Default domain
|
|
103
|
+
# - language: System language
|
|
104
|
+
# - ntp-server: Default NTP server
|
|
105
|
+
#
|
|
106
|
+
# @return [Hash] Default configuration with snake_case keys
|
|
107
|
+
# @example
|
|
108
|
+
# defaults = client.system.defaults
|
|
109
|
+
# # => { system_name: "Keenetic", domain_name: "local", language: "en", ... }
|
|
110
|
+
#
|
|
111
|
+
def defaults
|
|
112
|
+
response = get('/rci/show/defaults')
|
|
113
|
+
normalize_defaults(response)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get license status and enabled features.
|
|
117
|
+
#
|
|
118
|
+
# == Keenetic API Request
|
|
119
|
+
# GET /rci/show/license
|
|
120
|
+
#
|
|
121
|
+
# == Response Fields from API
|
|
122
|
+
# - valid: Boolean - license is valid
|
|
123
|
+
# - active: Boolean - license is active
|
|
124
|
+
# - expires: Expiration date string
|
|
125
|
+
# - type: License type (e.g., "standard")
|
|
126
|
+
# - features: Array of enabled features
|
|
127
|
+
# - services: Array of service statuses
|
|
128
|
+
#
|
|
129
|
+
# @return [Hash] License information
|
|
130
|
+
# @example
|
|
131
|
+
# license = client.system.license
|
|
132
|
+
# # => { valid: true, active: true, expires: "2025-12-31",
|
|
133
|
+
# # features: [{ name: "vpn-server", enabled: true }],
|
|
134
|
+
# # services: [{ name: "keendns", enabled: true, active: true }] }
|
|
135
|
+
#
|
|
136
|
+
def license
|
|
137
|
+
response = get('/rci/show/license')
|
|
138
|
+
normalize_license(response)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def normalize_resources(response)
|
|
144
|
+
return {} unless response.is_a?(Hash)
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
cpu: normalize_cpu(response['cpuload']),
|
|
148
|
+
memory: normalize_memory(response),
|
|
149
|
+
swap: normalize_swap(response),
|
|
150
|
+
uptime: response['uptime']
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def normalize_cpu(cpuload)
|
|
155
|
+
return nil unless cpuload
|
|
156
|
+
|
|
157
|
+
{
|
|
158
|
+
load_percent: cpuload.to_i
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def normalize_memory(response)
|
|
163
|
+
total = response['memtotal']
|
|
164
|
+
free = response['memfree']
|
|
165
|
+
buffers = response['membuffers'] || 0
|
|
166
|
+
cached = response['memcache'] || 0
|
|
167
|
+
|
|
168
|
+
return nil unless total && free
|
|
169
|
+
|
|
170
|
+
used = total - free - buffers - cached
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
total: total,
|
|
174
|
+
free: free,
|
|
175
|
+
used: used,
|
|
176
|
+
buffers: buffers,
|
|
177
|
+
cached: cached,
|
|
178
|
+
used_percent: ((used.to_f / total) * 100).round(1)
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def normalize_swap(response)
|
|
183
|
+
total = response['swaptotal']
|
|
184
|
+
free = response['swapfree']
|
|
185
|
+
|
|
186
|
+
return nil unless total && free && total > 0
|
|
187
|
+
|
|
188
|
+
used = total - free
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
total: total,
|
|
192
|
+
free: free,
|
|
193
|
+
used: used,
|
|
194
|
+
used_percent: ((used.to_f / total) * 100).round(1)
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def normalize_info(response)
|
|
199
|
+
return {} unless response.is_a?(Hash)
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
model: response['model'],
|
|
203
|
+
device: response['device'],
|
|
204
|
+
manufacturer: response['manufacturer'],
|
|
205
|
+
vendor: response['vendor'],
|
|
206
|
+
hw_version: response['hw_version'],
|
|
207
|
+
hw_id: response['hw_id'],
|
|
208
|
+
firmware: response['title'],
|
|
209
|
+
firmware_version: response['release'],
|
|
210
|
+
ndm_version: response.dig('ndm', 'exact') || response.dig('ndm', 'version'),
|
|
211
|
+
arch: response['arch'],
|
|
212
|
+
ndw_version: response.dig('ndw', 'version'),
|
|
213
|
+
components: response['components'],
|
|
214
|
+
sandbox: response['sandbox']
|
|
215
|
+
}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def normalize_defaults(response)
|
|
219
|
+
return {} unless response.is_a?(Hash)
|
|
220
|
+
|
|
221
|
+
deep_normalize_keys(response)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def normalize_license(response)
|
|
225
|
+
return {} unless response.is_a?(Hash)
|
|
226
|
+
|
|
227
|
+
result = {
|
|
228
|
+
valid: normalize_boolean(response['valid']),
|
|
229
|
+
active: normalize_boolean(response['active']),
|
|
230
|
+
expires: response['expires'],
|
|
231
|
+
type: response['type'],
|
|
232
|
+
features: normalize_features(response['features']),
|
|
233
|
+
services: normalize_services(response['services'])
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Remove nil values for cleaner response
|
|
237
|
+
result.compact
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def normalize_features(features)
|
|
241
|
+
return [] unless features.is_a?(Array)
|
|
242
|
+
|
|
243
|
+
features.map do |feature|
|
|
244
|
+
if feature.is_a?(Hash)
|
|
245
|
+
normalize_keys(feature)
|
|
246
|
+
else
|
|
247
|
+
feature
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def normalize_services(services)
|
|
253
|
+
return [] unless services.is_a?(Array)
|
|
254
|
+
|
|
255
|
+
services.map do |service|
|
|
256
|
+
if service.is_a?(Hash)
|
|
257
|
+
result = normalize_keys(service)
|
|
258
|
+
normalize_booleans(result, %i[enabled active])
|
|
259
|
+
else
|
|
260
|
+
service
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
module Keenetic
|
|
2
|
+
module Resources
|
|
3
|
+
# Wi-Fi resource for accessing wireless network information.
|
|
4
|
+
#
|
|
5
|
+
# == API Endpoints Used
|
|
6
|
+
#
|
|
7
|
+
# === Reading Wi-Fi Access Points
|
|
8
|
+
# GET /rci/show/interface
|
|
9
|
+
# Filters: interfaces where type == "AccessPoint" or id starts with "WifiMaster"
|
|
10
|
+
# Returns Wi-Fi specific fields: ssid, channel, band, authentication, encryption, station-count
|
|
11
|
+
#
|
|
12
|
+
# === Reading Connected Clients (Associations)
|
|
13
|
+
# GET /rci/show/associations
|
|
14
|
+
# Returns: { "station": [...] } - array of connected Wi-Fi clients
|
|
15
|
+
# Station fields: mac, ap, rssi, txrate, rxrate, uptime, ht/vht/he mode flags
|
|
16
|
+
#
|
|
17
|
+
# == Wi-Fi Interface Naming
|
|
18
|
+
# - WifiMaster0: First Wi-Fi radio (usually 2.4GHz)
|
|
19
|
+
# - WifiMaster1: Second Wi-Fi radio (usually 5GHz)
|
|
20
|
+
# - WifiMaster0/AccessPoint0: Main SSID on first radio
|
|
21
|
+
# - WifiMaster0/AccessPoint1: Guest SSID on first radio
|
|
22
|
+
#
|
|
23
|
+
class WiFi < Base
|
|
24
|
+
# Get all Wi-Fi access points.
|
|
25
|
+
#
|
|
26
|
+
# == Keenetic API Request
|
|
27
|
+
# GET /rci/show/interface
|
|
28
|
+
# Internally filters for Wi-Fi interfaces only
|
|
29
|
+
#
|
|
30
|
+
# == Wi-Fi Specific Fields from API
|
|
31
|
+
# - ssid: Network name
|
|
32
|
+
# - channel: Wi-Fi channel number
|
|
33
|
+
# - band: Frequency band ("2.4GHz", "5GHz")
|
|
34
|
+
# - authentication: Security mode (wpa2-psk, wpa3-psk, etc.)
|
|
35
|
+
# - encryption: Encryption type (aes, tkip)
|
|
36
|
+
# - station-count: Number of connected clients
|
|
37
|
+
# - txpower: Transmit power in dBm
|
|
38
|
+
#
|
|
39
|
+
# @return [Array<Hash>] List of Wi-Fi access points
|
|
40
|
+
# @example
|
|
41
|
+
# aps = client.wifi.access_points
|
|
42
|
+
# # => [{ id: "WifiMaster0/AccessPoint0", ssid: "MyNetwork", channel: 6, band: "2.4GHz", ... }]
|
|
43
|
+
#
|
|
44
|
+
def access_points
|
|
45
|
+
response = get('/rci/show/interface')
|
|
46
|
+
extract_wifi_interfaces(response)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get Mesh Wi-Fi System members (controller + extenders).
|
|
50
|
+
#
|
|
51
|
+
# == Keenetic API Request
|
|
52
|
+
# POST /rci/
|
|
53
|
+
# Body: [{"show":{"mws":{"member":{}}}}, {"show":{"mws":{"status":{}}}}]
|
|
54
|
+
#
|
|
55
|
+
# == Response Structure from API
|
|
56
|
+
# Member data includes:
|
|
57
|
+
# - known: true/false (if node is registered)
|
|
58
|
+
# - online: true/false
|
|
59
|
+
# - cid: unique controller ID
|
|
60
|
+
# - mac: device MAC address
|
|
61
|
+
# - hw-id: hardware ID
|
|
62
|
+
# - hw-version: hardware version
|
|
63
|
+
# - model: device model (e.g., "KN-4010", "KN-1613")
|
|
64
|
+
# - name: device name
|
|
65
|
+
# - mode: operating mode ("controller", "extender")
|
|
66
|
+
# - via: connection path (e.g., "Ethernet", "WifiMaster1")
|
|
67
|
+
# - ip: device IP address
|
|
68
|
+
# - uptime: uptime in seconds
|
|
69
|
+
# - version: firmware version
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<Hash>] List of mesh nodes (controller + extenders)
|
|
72
|
+
# @example
|
|
73
|
+
# nodes = client.wifi.mesh_members
|
|
74
|
+
# # => [{ id: "abc123", name: "Main Router", mode: "controller", ... }]
|
|
75
|
+
#
|
|
76
|
+
def mesh_members
|
|
77
|
+
responses = client.batch([
|
|
78
|
+
{ 'show' => { 'mws' => { 'member' => {} } } },
|
|
79
|
+
{ 'show' => { 'version' => {} } },
|
|
80
|
+
{ 'show' => { 'system' => {} } },
|
|
81
|
+
{ 'show' => { 'associations' => {} } }
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
members_response = responses[0] || {}
|
|
85
|
+
version_response = responses[1] || {}
|
|
86
|
+
system_response = responses[2] || {}
|
|
87
|
+
associations_response = responses[3] || {}
|
|
88
|
+
|
|
89
|
+
# Get extenders from mws member
|
|
90
|
+
extenders = normalize_mesh_members(members_response)
|
|
91
|
+
|
|
92
|
+
# Build controller from version/system info
|
|
93
|
+
controller = build_controller(version_response, system_response, associations_response)
|
|
94
|
+
|
|
95
|
+
# Return controller first, then extenders
|
|
96
|
+
[controller, *extenders].compact
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get connected Wi-Fi clients (associations).
|
|
100
|
+
#
|
|
101
|
+
# == Keenetic API Request
|
|
102
|
+
# GET /rci/show/associations
|
|
103
|
+
#
|
|
104
|
+
# == Response Structure from API
|
|
105
|
+
# {
|
|
106
|
+
# "station": [
|
|
107
|
+
# {
|
|
108
|
+
# "mac": "AA:BB:CC:DD:EE:FF",
|
|
109
|
+
# "ap": "WifiMaster0/AccessPoint0",
|
|
110
|
+
# "authenticated": true,
|
|
111
|
+
# "txrate": 866700,
|
|
112
|
+
# "rxrate": 780000,
|
|
113
|
+
# "rssi": -45,
|
|
114
|
+
# "uptime": 3600,
|
|
115
|
+
# "mcs": 9,
|
|
116
|
+
# "ht": false,
|
|
117
|
+
# "vht": true,
|
|
118
|
+
# "mode": "ac",
|
|
119
|
+
# "gi": "short"
|
|
120
|
+
# }
|
|
121
|
+
# ]
|
|
122
|
+
# }
|
|
123
|
+
#
|
|
124
|
+
# == Signal Strength (RSSI)
|
|
125
|
+
# - rssi: Signal strength in dBm (negative value, closer to 0 is stronger)
|
|
126
|
+
# - Typical ranges: -30 to -50 (excellent), -50 to -70 (good), -70 to -80 (fair)
|
|
127
|
+
#
|
|
128
|
+
# @return [Array<Hash>] List of connected Wi-Fi clients
|
|
129
|
+
# @example
|
|
130
|
+
# clients = client.wifi.clients
|
|
131
|
+
# # => [{ mac: "AA:BB:CC:DD:EE:FF", ap: "WifiMaster0/AccessPoint0", rssi: -45, ... }]
|
|
132
|
+
#
|
|
133
|
+
def clients
|
|
134
|
+
response = get('/rci/show/associations')
|
|
135
|
+
normalize_clients(response)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Get specific Wi-Fi access point by ID.
|
|
139
|
+
#
|
|
140
|
+
# @param id [String] Access point ID (e.g., "WifiMaster0/AccessPoint0")
|
|
141
|
+
# @return [Hash, nil] Access point data or nil if not found
|
|
142
|
+
# @example
|
|
143
|
+
# ap = client.wifi.access_point('WifiMaster0/AccessPoint0')
|
|
144
|
+
# # => { id: "WifiMaster0/AccessPoint0", ssid: "MyNetwork", ... }
|
|
145
|
+
#
|
|
146
|
+
def access_point(id)
|
|
147
|
+
access_points.find { |ap| ap[:id] == id }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Configure Wi-Fi access point settings.
|
|
151
|
+
#
|
|
152
|
+
# == Keenetic API Request
|
|
153
|
+
# POST /rci/ (batch format)
|
|
154
|
+
# Body: [{"interface": {"<ap_id>": {<config>}}}]
|
|
155
|
+
#
|
|
156
|
+
# == Authentication Types
|
|
157
|
+
# - "open": No security
|
|
158
|
+
# - "wpa-psk": WPA Personal
|
|
159
|
+
# - "wpa2-psk": WPA2 Personal
|
|
160
|
+
# - "wpa3-psk": WPA3 Personal
|
|
161
|
+
# - "wpa2/wpa3-psk": WPA2/WPA3 mixed mode
|
|
162
|
+
#
|
|
163
|
+
# == Encryption Types
|
|
164
|
+
# - "aes": AES encryption (recommended)
|
|
165
|
+
# - "tkip": TKIP encryption (legacy)
|
|
166
|
+
#
|
|
167
|
+
# @param id [String] Access point ID (e.g., "WifiMaster0/AccessPoint0")
|
|
168
|
+
# @param options [Hash] Configuration options
|
|
169
|
+
# @option options [String] :ssid Network name
|
|
170
|
+
# @option options [String] :authentication Security mode
|
|
171
|
+
# @option options [String] :encryption Encryption type
|
|
172
|
+
# @option options [String] :key Wi-Fi password
|
|
173
|
+
# @option options [Integer] :channel Channel number (0 for auto)
|
|
174
|
+
# @option options [Boolean] :up Enable or disable the access point
|
|
175
|
+
# @return [Array<Hash>] API response
|
|
176
|
+
#
|
|
177
|
+
# @example Configure access point
|
|
178
|
+
# client.wifi.configure('WifiMaster0/AccessPoint0',
|
|
179
|
+
# ssid: 'MyNetwork',
|
|
180
|
+
# authentication: 'wpa2-psk',
|
|
181
|
+
# encryption: 'aes',
|
|
182
|
+
# key: 'mysecretpassword',
|
|
183
|
+
# up: true
|
|
184
|
+
# )
|
|
185
|
+
# # Sends: [{"interface":{"WifiMaster0/AccessPoint0":{"ssid":"MyNetwork",...}}}]
|
|
186
|
+
#
|
|
187
|
+
# @example Change SSID only
|
|
188
|
+
# client.wifi.configure('WifiMaster0/AccessPoint0', ssid: 'NewNetworkName')
|
|
189
|
+
#
|
|
190
|
+
# @example Set channel
|
|
191
|
+
# client.wifi.configure('WifiMaster0', channel: 6)
|
|
192
|
+
#
|
|
193
|
+
def configure(id, **options)
|
|
194
|
+
params = {}
|
|
195
|
+
|
|
196
|
+
params['ssid'] = options[:ssid] if options[:ssid]
|
|
197
|
+
params['authentication'] = options[:authentication] if options[:authentication]
|
|
198
|
+
params['encryption'] = options[:encryption] if options[:encryption]
|
|
199
|
+
params['key'] = options[:key] if options[:key]
|
|
200
|
+
params['channel'] = options[:channel] if options[:channel]
|
|
201
|
+
params['up'] = options[:up] unless options[:up].nil?
|
|
202
|
+
|
|
203
|
+
return {} if params.empty?
|
|
204
|
+
|
|
205
|
+
client.batch([{ 'interface' => { id => params } }])
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Enable a Wi-Fi access point.
|
|
209
|
+
#
|
|
210
|
+
# == Keenetic API Request
|
|
211
|
+
# POST /rci/ (batch format)
|
|
212
|
+
# Body: [{"interface": {"<ap_id>": {"up": true}}}]
|
|
213
|
+
#
|
|
214
|
+
# @param id [String] Access point ID (e.g., "WifiMaster0/AccessPoint0")
|
|
215
|
+
# @return [Array<Hash>] API response
|
|
216
|
+
#
|
|
217
|
+
# @example Enable access point
|
|
218
|
+
# client.wifi.enable('WifiMaster0/AccessPoint0')
|
|
219
|
+
# # Sends: [{"interface":{"WifiMaster0/AccessPoint0":{"up":true}}}]
|
|
220
|
+
#
|
|
221
|
+
def enable(id)
|
|
222
|
+
client.batch([{ 'interface' => { id => { 'up' => true } } }])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Disable a Wi-Fi access point.
|
|
226
|
+
#
|
|
227
|
+
# == Keenetic API Request
|
|
228
|
+
# POST /rci/ (batch format)
|
|
229
|
+
# Body: [{"interface": {"<ap_id>": {"up": false}}}]
|
|
230
|
+
#
|
|
231
|
+
# @param id [String] Access point ID (e.g., "WifiMaster0/AccessPoint0")
|
|
232
|
+
# @return [Array<Hash>] API response
|
|
233
|
+
#
|
|
234
|
+
# @example Disable guest network
|
|
235
|
+
# client.wifi.disable('WifiMaster0/AccessPoint1')
|
|
236
|
+
# # Sends: [{"interface":{"WifiMaster0/AccessPoint1":{"up":false}}}]
|
|
237
|
+
#
|
|
238
|
+
def disable(id)
|
|
239
|
+
client.batch([{ 'interface' => { id => { 'up' => false } } }])
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def extract_wifi_interfaces(response)
|
|
245
|
+
return [] unless response.is_a?(Hash)
|
|
246
|
+
|
|
247
|
+
response
|
|
248
|
+
.select { |id, data| wifi_interface?(id, data) }
|
|
249
|
+
.map { |id, data| normalize_wifi(id, data) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def wifi_interface?(id, data)
|
|
253
|
+
return false unless data.is_a?(Hash)
|
|
254
|
+
|
|
255
|
+
data['type'] == 'AccessPoint' ||
|
|
256
|
+
data['type'] == 'WifiMaster' ||
|
|
257
|
+
id.start_with?('WifiMaster') ||
|
|
258
|
+
id.start_with?('AccessPoint')
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def normalize_wifi(id, data)
|
|
262
|
+
{
|
|
263
|
+
id: id,
|
|
264
|
+
description: data['description'],
|
|
265
|
+
type: data['type'],
|
|
266
|
+
ssid: data['ssid'],
|
|
267
|
+
mac: data['mac'],
|
|
268
|
+
state: data['state'],
|
|
269
|
+
link: data['link'],
|
|
270
|
+
connected: data['connected'],
|
|
271
|
+
channel: data['channel'],
|
|
272
|
+
band: data['band'],
|
|
273
|
+
security: data['authentication'],
|
|
274
|
+
encryption: data['encryption'],
|
|
275
|
+
clients_count: data['station-count'],
|
|
276
|
+
txpower: data['txpower'],
|
|
277
|
+
uptime: data['uptime']
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def normalize_clients(response)
|
|
282
|
+
return [] unless response.is_a?(Hash) && response['station']
|
|
283
|
+
|
|
284
|
+
stations = response['station']
|
|
285
|
+
stations = [stations] unless stations.is_a?(Array)
|
|
286
|
+
|
|
287
|
+
stations.map { |station| normalize_client(station) }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def normalize_client(station)
|
|
291
|
+
{
|
|
292
|
+
mac: station['mac'],
|
|
293
|
+
ap: station['ap'],
|
|
294
|
+
authenticated: station['authenticated'],
|
|
295
|
+
txrate: station['txrate'],
|
|
296
|
+
rxrate: station['rxrate'],
|
|
297
|
+
uptime: station['uptime'],
|
|
298
|
+
txbytes: station['txbytes'],
|
|
299
|
+
rxbytes: station['rxbytes'],
|
|
300
|
+
rssi: station['rssi'],
|
|
301
|
+
mcs: station['mcs'],
|
|
302
|
+
ht: station['ht'],
|
|
303
|
+
mode: station['mode'],
|
|
304
|
+
gi: station['gi']
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def normalize_mesh_members(members_response)
|
|
309
|
+
# Response is nested: { "show" => { "mws" => { "member" => [...] } } }
|
|
310
|
+
members_data = members_response.dig('show', 'mws', 'member') if members_response.is_a?(Hash)
|
|
311
|
+
return [] unless members_data
|
|
312
|
+
|
|
313
|
+
members = members_data.is_a?(Array) ? members_data : [members_data]
|
|
314
|
+
|
|
315
|
+
members.filter_map { |member| normalize_mesh_member(member) }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def build_controller(version_response, system_response, associations_response)
|
|
319
|
+
version = version_response.dig('show', 'version') || {}
|
|
320
|
+
system = system_response.dig('show', 'system') || {}
|
|
321
|
+
|
|
322
|
+
# Count associations (wifi clients connected to controller)
|
|
323
|
+
stations = associations_response.dig('show', 'associations', 'station') || []
|
|
324
|
+
stations = [stations] unless stations.is_a?(Array)
|
|
325
|
+
client_count = stations.size
|
|
326
|
+
|
|
327
|
+
return nil if version.empty?
|
|
328
|
+
|
|
329
|
+
{
|
|
330
|
+
id: 'controller',
|
|
331
|
+
mac: version['mac'],
|
|
332
|
+
name: system['name'] || version['description'] || version['model'],
|
|
333
|
+
model: "#{version['model']} (#{version['hw_id']})",
|
|
334
|
+
hw_id: version['hw_id'],
|
|
335
|
+
hw_version: version['hw_version'],
|
|
336
|
+
mode: 'controller',
|
|
337
|
+
via: nil, # Controller doesn't have upstream
|
|
338
|
+
ip: nil, # Controller is the gateway
|
|
339
|
+
version: version['release'],
|
|
340
|
+
online: true,
|
|
341
|
+
uptime: system['uptime'],
|
|
342
|
+
clients_count: client_count,
|
|
343
|
+
connection_speed: nil,
|
|
344
|
+
alert: false
|
|
345
|
+
}
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def normalize_mesh_member(member)
|
|
349
|
+
return nil unless member.is_a?(Hash)
|
|
350
|
+
|
|
351
|
+
cid = member['cid']
|
|
352
|
+
system_info = member['system'] || {}
|
|
353
|
+
backhaul_info = member['backhaul'] || {}
|
|
354
|
+
|
|
355
|
+
{
|
|
356
|
+
id: cid,
|
|
357
|
+
mac: member['mac'],
|
|
358
|
+
name: member['known-host'] || member['model'],
|
|
359
|
+
model: member['model'],
|
|
360
|
+
hw_id: member['hw_id'] || member['hw-id'],
|
|
361
|
+
hw_version: member['hw-version'],
|
|
362
|
+
mode: member['mode'], # "controller" or "extender"
|
|
363
|
+
via: backhaul_info['uplink'], # Connection method (FastEthernet0/Vlan1, WifiMaster1, etc.)
|
|
364
|
+
ip: member['ip'],
|
|
365
|
+
version: member['fw'],
|
|
366
|
+
online: true, # Members in the list are online
|
|
367
|
+
uptime: system_info['uptime']&.to_i,
|
|
368
|
+
clients_count: member['associations'],
|
|
369
|
+
connection_speed: backhaul_info['speed'],
|
|
370
|
+
alert: false
|
|
371
|
+
}
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|