rshelly 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/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +207 -0
- data/exe/shelly +8 -0
- data/lib/rshelly/cli.rb +1067 -0
- data/lib/rshelly/client.rb +400 -0
- data/lib/rshelly/discovery.rb +371 -0
- data/lib/rshelly/error.rb +9 -0
- data/lib/rshelly/version.rb +5 -0
- data/lib/rshelly.rb +20 -0
- metadata +59 -0
data/lib/rshelly/cli.rb
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../rshelly"
|
|
6
|
+
|
|
7
|
+
module Shelly
|
|
8
|
+
class CLI
|
|
9
|
+
COMMAND_ALIASES = {
|
|
10
|
+
"temperature" => "temp",
|
|
11
|
+
"get" => "gen1"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
15
|
+
@argv = argv.dup
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@stderr = stderr
|
|
18
|
+
@options = { timeout: 5, json: false }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
parse_global_options!
|
|
23
|
+
command = @argv.shift
|
|
24
|
+
return command_help(command) if command && consume_help_flag!
|
|
25
|
+
|
|
26
|
+
case command
|
|
27
|
+
when "discover" then discover
|
|
28
|
+
when "scan" then scan
|
|
29
|
+
when "info" then with_client { |client| output(client.normalized_info, :info) }
|
|
30
|
+
when "status" then with_client { |client| output(client.status, :status, host: client.host) }
|
|
31
|
+
when "config" then with_client { |client| output(client.config, :config, host: client.host) }
|
|
32
|
+
when "switch" then switch
|
|
33
|
+
when "meter" then meter
|
|
34
|
+
when "temp", "temperature" then temperature
|
|
35
|
+
when "rgbw" then rgbw
|
|
36
|
+
when "light" then light
|
|
37
|
+
when "update" then update
|
|
38
|
+
when "reboot" then reboot
|
|
39
|
+
when "wifi" then wifi
|
|
40
|
+
when "rpc" then rpc
|
|
41
|
+
when "gen1", "get" then gen1_get
|
|
42
|
+
when "mqtt" then mqtt
|
|
43
|
+
when "input" then input
|
|
44
|
+
when "webhook" then webhook
|
|
45
|
+
when "help" then command_help(@argv.shift)
|
|
46
|
+
when nil then help
|
|
47
|
+
when "version", "--version", "-v"
|
|
48
|
+
@stdout.puts VERSION
|
|
49
|
+
0
|
|
50
|
+
else
|
|
51
|
+
raise Error, "unknown command: #{command}"
|
|
52
|
+
end
|
|
53
|
+
rescue Error, OptionParser::ParseError, ArgumentError, JSON::ParserError => e
|
|
54
|
+
@stderr.puts "shelly: #{e.message}"
|
|
55
|
+
@stderr.puts "Run `shelly help` for usage."
|
|
56
|
+
1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def parse_global_options!
|
|
62
|
+
parser = OptionParser.new do |opts|
|
|
63
|
+
opts.on("--json", "Print JSON") { @options[:json] = true }
|
|
64
|
+
opts.on("--timeout SECONDS", Float, "HTTP/discovery timeout") { |value| @options[:timeout] = value }
|
|
65
|
+
opts.on("-h", "--help", "Print help") { @options[:help] = true }
|
|
66
|
+
opts.on("-v", "--version", "Print version") { @options[:version] = true }
|
|
67
|
+
end
|
|
68
|
+
parser.order!(@argv)
|
|
69
|
+
@argv.unshift("version") if @options[:version]
|
|
70
|
+
@argv.unshift("help") if @options[:help]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def discover
|
|
74
|
+
verify = consume_flag!("--verify")
|
|
75
|
+
concurrency = consume_option!("--concurrency", Integer) || Discovery::DEFAULT_CONCURRENCY
|
|
76
|
+
raise Error, "unexpected arguments: #{@argv.join(" ")}" unless @argv.empty?
|
|
77
|
+
|
|
78
|
+
output(
|
|
79
|
+
Discovery.discover(
|
|
80
|
+
timeout: @options[:timeout],
|
|
81
|
+
verify: verify,
|
|
82
|
+
concurrency: concurrency,
|
|
83
|
+
client_options: client_options
|
|
84
|
+
),
|
|
85
|
+
verify ? :devices : :discovery
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def scan
|
|
90
|
+
concurrency = consume_option!("--concurrency", Integer) || Discovery::DEFAULT_CONCURRENCY
|
|
91
|
+
max_hosts = consume_option!("--max-hosts", Integer) || 4096
|
|
92
|
+
network = @argv.shift
|
|
93
|
+
raise Error, "unexpected arguments: #{@argv.join(" ")}" unless @argv.empty?
|
|
94
|
+
|
|
95
|
+
output(Discovery.scan(
|
|
96
|
+
network: network,
|
|
97
|
+
timeout: @options[:timeout],
|
|
98
|
+
concurrency: concurrency,
|
|
99
|
+
max_hosts: max_hosts,
|
|
100
|
+
client_options: client_options
|
|
101
|
+
), :devices)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def switch
|
|
105
|
+
host = require_arg!("HOST")
|
|
106
|
+
action = require_arg!("ACTION")
|
|
107
|
+
id = consume_option!("--id", Integer) || 0
|
|
108
|
+
client = Client.new(host, **client_options)
|
|
109
|
+
|
|
110
|
+
result = case action
|
|
111
|
+
when "get", "status" then client.switch_status(id)
|
|
112
|
+
when "on" then client.switch_set(id: id, on: true)
|
|
113
|
+
when "off" then client.switch_set(id: id, on: false)
|
|
114
|
+
when "toggle" then client.switch_toggle(id)
|
|
115
|
+
else raise Error, "switch action must be get, on, off, or toggle"
|
|
116
|
+
end
|
|
117
|
+
output(result, :switch, host: host, action: action, id: id)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def meter
|
|
121
|
+
host = require_arg!("HOST")
|
|
122
|
+
id = consume_option!("--id", Integer) || 0
|
|
123
|
+
output(Client.new(host, **client_options).meter_status(id), :meter, host: host, id: id)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def temperature
|
|
127
|
+
host = require_arg!("HOST")
|
|
128
|
+
id = consume_option!("--id", Integer) || 100
|
|
129
|
+
output(Client.new(host, **client_options).temperature_status(id: id), :temperature, host: host, id: id)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def rgbw
|
|
133
|
+
host = require_arg!("HOST")
|
|
134
|
+
action = require_arg!("ACTION")
|
|
135
|
+
client = Client.new(host, **client_options)
|
|
136
|
+
|
|
137
|
+
case action
|
|
138
|
+
when "get", "status"
|
|
139
|
+
id = consume_option!("--id", Integer) || 0
|
|
140
|
+
output(client.rgbw_status(id), :rgbw, host: host, action: action, id: id)
|
|
141
|
+
when "set"
|
|
142
|
+
opts = parse_rgbw_set_options
|
|
143
|
+
output(client.rgbw_set(**opts), :write, action: "RGBW set", host: host)
|
|
144
|
+
else
|
|
145
|
+
raise Error, "rgbw action must be get or set"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def light
|
|
150
|
+
host = require_arg!("HOST")
|
|
151
|
+
action = require_arg!("ACTION")
|
|
152
|
+
id = consume_option!("--id", Integer) || 0
|
|
153
|
+
client = Client.new(host, **client_options)
|
|
154
|
+
|
|
155
|
+
case action
|
|
156
|
+
when "get", "status"
|
|
157
|
+
output(client.light_status(id), :light, host: host, action: action, id: id)
|
|
158
|
+
when "set"
|
|
159
|
+
on = consume_flag!("--on") ? true : nil
|
|
160
|
+
on = false if consume_flag!("--off")
|
|
161
|
+
brightness = consume_option!("--brightness", Integer)
|
|
162
|
+
transition_duration = consume_option!("--transition", Float)
|
|
163
|
+
output(client.light_set(id: id, on: on, brightness: brightness, transition_duration: transition_duration), :write, action: "Light set", host: host)
|
|
164
|
+
else
|
|
165
|
+
raise Error, "light action must be get or set"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def update
|
|
170
|
+
action = require_arg!("ACTION")
|
|
171
|
+
host = require_arg!("HOST")
|
|
172
|
+
client = Client.new(host, **client_options)
|
|
173
|
+
|
|
174
|
+
case action
|
|
175
|
+
when "check" then output(client.update_info, :update_check, host: host)
|
|
176
|
+
when "start" then output(client.update_start(stage: consume_option!("--stage") || "stable"), :write, action: "Update started", host: host)
|
|
177
|
+
else raise Error, "update action must be check or start"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def reboot
|
|
182
|
+
host = require_arg!("HOST")
|
|
183
|
+
output(Client.new(host, **client_options).reboot, :write, action: "Reboot requested", host: host)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def wifi
|
|
187
|
+
options = parse_wifi_options
|
|
188
|
+
host = require_arg!("HOST")
|
|
189
|
+
slot = require_arg!("primary|secondary")
|
|
190
|
+
ssid = require_arg!("SSID")
|
|
191
|
+
password = @argv.shift
|
|
192
|
+
raise Error, "missing PASSWORD" unless password
|
|
193
|
+
raise Error, "unexpected arguments: #{@argv.join(" ")}" unless @argv.empty?
|
|
194
|
+
|
|
195
|
+
output(Client.new(host, **client_options).set_wifi(
|
|
196
|
+
slot: slot,
|
|
197
|
+
ssid: ssid,
|
|
198
|
+
password: password,
|
|
199
|
+
ip: options[:ip],
|
|
200
|
+
netmask: options[:netmask],
|
|
201
|
+
gw: options[:gw],
|
|
202
|
+
dns: options[:dns]
|
|
203
|
+
), :write, action: "Wi-Fi #{slot} configured", host: host)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def rpc
|
|
207
|
+
host = require_arg!("HOST")
|
|
208
|
+
method = require_arg!("METHOD")
|
|
209
|
+
params = @argv.empty? ? nil : parse_json_or_file(@argv.shift)
|
|
210
|
+
output(Client.new(host, **client_options).rpc(method, params), :raw, title: "#{host} #{method}")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def gen1_get
|
|
214
|
+
host = require_arg!("HOST")
|
|
215
|
+
path = require_arg!("PATH")
|
|
216
|
+
params = parse_key_values(@argv)
|
|
217
|
+
output(Client.new(host, **client_options).get_json(path, params), :raw, title: "#{host} #{path}")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def mqtt
|
|
221
|
+
host = require_arg!("HOST")
|
|
222
|
+
server = require_arg!("SERVER")
|
|
223
|
+
enable = !consume_flag!("--disable")
|
|
224
|
+
output(Client.new(host, **client_options).enable_mqtt(server: server, enable: enable), :write, action: enable ? "MQTT configured" : "MQTT disabled", host: host)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def input
|
|
228
|
+
action = require_arg!("ACTION")
|
|
229
|
+
raise Error, "input action must be setup" unless action == "setup"
|
|
230
|
+
|
|
231
|
+
host = require_arg!("HOST")
|
|
232
|
+
device_id = consume_option!("--device-id") || consume_option!("--prefix") || require_arg!("DEVICE_ID")
|
|
233
|
+
base_url = consume_option!("--base-url") || "http://home.home/inputs"
|
|
234
|
+
count = consume_option!("--count", Integer) || 4
|
|
235
|
+
client = Client.new(host, **client_options)
|
|
236
|
+
|
|
237
|
+
client.set_device_name("input_device_#{device_id}")
|
|
238
|
+
client.webhook_delete_all
|
|
239
|
+
created = []
|
|
240
|
+
|
|
241
|
+
(0...count).each do |cid|
|
|
242
|
+
input_name = "input_#{cid + 1}"
|
|
243
|
+
input_id = "#{device_id}-#{cid + 1}"
|
|
244
|
+
client.input_set_config(id: cid, type: "button", name: input_name)
|
|
245
|
+
|
|
246
|
+
%w[push longpush doublepush].each do |event|
|
|
247
|
+
url = "#{base_url}/#{input_id}/#{event}"
|
|
248
|
+
client.webhook_create(
|
|
249
|
+
cid: cid,
|
|
250
|
+
name: event,
|
|
251
|
+
event: "input.button_#{event}",
|
|
252
|
+
urls: [url]
|
|
253
|
+
)
|
|
254
|
+
created << { input: input_id, cid: cid, event: event, url: url }
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
output(created, :input_setup, host: host, device_id: device_id)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def webhook
|
|
262
|
+
action = require_arg!("ACTION")
|
|
263
|
+
host = require_arg!("HOST")
|
|
264
|
+
client = Client.new(host, **client_options)
|
|
265
|
+
|
|
266
|
+
case action
|
|
267
|
+
when "delete-all" then output(client.webhook_delete_all, :write, action: "Webhooks deleted", host: host)
|
|
268
|
+
else raise Error, "webhook action must be delete-all"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def with_client
|
|
273
|
+
host = require_arg!("HOST")
|
|
274
|
+
yield Client.new(host, **client_options)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def parse_rgbw_set_options
|
|
278
|
+
result = { id: 0 }
|
|
279
|
+
parser = OptionParser.new do |opts|
|
|
280
|
+
opts.on("--id ID", Integer) { |value| result[:id] = value }
|
|
281
|
+
opts.on("--on") { result[:on] = true }
|
|
282
|
+
opts.on("--off") { result[:on] = false }
|
|
283
|
+
opts.on("--brightness N", Integer) { |value| result[:brightness] = value }
|
|
284
|
+
opts.on("--white N", Integer) { |value| result[:white] = value }
|
|
285
|
+
opts.on("--red N", Integer) { |value| result[:red] = value }
|
|
286
|
+
opts.on("--green N", Integer) { |value| result[:green] = value }
|
|
287
|
+
opts.on("--blue N", Integer) { |value| result[:blue] = value }
|
|
288
|
+
opts.on("--transition SECONDS", Float) { |value| result[:transition_duration] = value }
|
|
289
|
+
end
|
|
290
|
+
parser.parse!(@argv)
|
|
291
|
+
result
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def parse_wifi_options
|
|
295
|
+
result = {}
|
|
296
|
+
parser = OptionParser.new do |opts|
|
|
297
|
+
opts.on("--ip IP") { |value| result[:ip] = value }
|
|
298
|
+
opts.on("--netmask MASK") { |value| result[:netmask] = value }
|
|
299
|
+
opts.on("--mask MASK") { |value| result[:netmask] = value }
|
|
300
|
+
opts.on("--gw IP") { |value| result[:gw] = value }
|
|
301
|
+
opts.on("--gateway IP") { |value| result[:gw] = value }
|
|
302
|
+
opts.on("--dns IP") { |value| result[:dns] = value }
|
|
303
|
+
opts.on("--nameserver IP") { |value| result[:dns] = value }
|
|
304
|
+
end
|
|
305
|
+
parser.parse!(@argv)
|
|
306
|
+
result
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def output(value, format = :raw, context = {})
|
|
310
|
+
if @options[:json]
|
|
311
|
+
@stdout.puts JSON.pretty_generate(value)
|
|
312
|
+
else
|
|
313
|
+
@stdout.puts human_output(value, format, context)
|
|
314
|
+
end
|
|
315
|
+
0
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def human_output(value, format, context)
|
|
319
|
+
case format
|
|
320
|
+
when :devices then devices_table(value)
|
|
321
|
+
when :discovery then discovery_table(value)
|
|
322
|
+
when :info then info_block(value)
|
|
323
|
+
when :status then status_block(value, context)
|
|
324
|
+
when :config then config_block(value, context)
|
|
325
|
+
when :switch then switch_line(value, context)
|
|
326
|
+
when :meter then meter_block(value, context)
|
|
327
|
+
when :temperature then temperature_block(value, context)
|
|
328
|
+
when :light, :rgbw then component_block(value, context)
|
|
329
|
+
when :update_check then update_check_block(value, context)
|
|
330
|
+
when :write then write_line(value, context)
|
|
331
|
+
when :input_setup then input_setup_block(value, context)
|
|
332
|
+
else raw_block(value, context[:title])
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def devices_table(devices)
|
|
337
|
+
rows = Array(devices).map do |device|
|
|
338
|
+
raw = device["raw"] || {}
|
|
339
|
+
wifi = raw["wifi"] || {}
|
|
340
|
+
update = device["update"] || {}
|
|
341
|
+
[
|
|
342
|
+
device["ip"],
|
|
343
|
+
device["gen"],
|
|
344
|
+
device["model"] || device["model_id"],
|
|
345
|
+
device["name"],
|
|
346
|
+
device["hostname"],
|
|
347
|
+
update_label(update),
|
|
348
|
+
wifi["ssid"] || raw.dig("wifi_sta", "ssid"),
|
|
349
|
+
wifi["rssi"] || raw.dig("wifi_sta", "rssi"),
|
|
350
|
+
short_version(device["firmware"] || update["current_version"])
|
|
351
|
+
]
|
|
352
|
+
end
|
|
353
|
+
table(%w[IP Gen Model Name Hostname Update Wi-Fi RSSI Firmware], rows)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def discovery_table(records)
|
|
357
|
+
rows = Array(records).map do |record|
|
|
358
|
+
[
|
|
359
|
+
record[:ip] || record["ip"],
|
|
360
|
+
record[:name] || record["name"],
|
|
361
|
+
record[:hostname] || record["hostname"],
|
|
362
|
+
record[:source] || record["source"],
|
|
363
|
+
record[:gen] || record["gen"],
|
|
364
|
+
record[:mac] || record["mac"]
|
|
365
|
+
]
|
|
366
|
+
end
|
|
367
|
+
table(%w[IP Name Hostname Source Gen MAC], rows)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def info_block(device)
|
|
371
|
+
raw = device["raw"] || {}
|
|
372
|
+
wifi = raw["wifi"] || {}
|
|
373
|
+
update = device["update"] || {}
|
|
374
|
+
lines = []
|
|
375
|
+
lines << (device["name"] || "Unnamed device").to_s
|
|
376
|
+
lines << ""
|
|
377
|
+
lines.concat(key_values([
|
|
378
|
+
["IP", device["ip"]],
|
|
379
|
+
["Model", model_label(device)],
|
|
380
|
+
["Model ID", device["model_id"]],
|
|
381
|
+
["Generation", device["gen"]],
|
|
382
|
+
["MAC", device["mac"]],
|
|
383
|
+
["Hostname", device["hostname"] || raw["id"] || raw.dig("device", "hostname")],
|
|
384
|
+
["Firmware", device["firmware"]],
|
|
385
|
+
["Update", update_label(update)],
|
|
386
|
+
["New Version", update["new_version"]],
|
|
387
|
+
["Wi-Fi", wifi_summary(wifi, raw)],
|
|
388
|
+
["Cloud", connection_label(raw["cloud"])],
|
|
389
|
+
["MQTT", connection_label(raw["mqtt"])],
|
|
390
|
+
["Uptime", duration(raw.dig("sys", "uptime"))],
|
|
391
|
+
["Restart Required", raw.dig("sys", "restart_required")],
|
|
392
|
+
["Config Rev", raw.dig("sys", "cfg_rev")]
|
|
393
|
+
]))
|
|
394
|
+
component_summary(raw).tap { |summary| lines.concat(["", summary]) unless summary.empty? }
|
|
395
|
+
lines.join("\n")
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def status_block(status, context)
|
|
399
|
+
lines = ["Status #{context[:host]}".strip, ""]
|
|
400
|
+
lines.concat(key_values([
|
|
401
|
+
["Wi-Fi", wifi_summary(status["wifi"] || {}, status)],
|
|
402
|
+
["Cloud", connection_label(status["cloud"])],
|
|
403
|
+
["MQTT", connection_label(status["mqtt"])],
|
|
404
|
+
["Uptime", duration(status.dig("sys", "uptime"))],
|
|
405
|
+
["Time", status.dig("sys", "time")],
|
|
406
|
+
["Restart Required", status.dig("sys", "restart_required")]
|
|
407
|
+
]))
|
|
408
|
+
component_summary(status).tap { |summary| lines.concat(["", summary]) unless summary.empty? }
|
|
409
|
+
lines.join("\n")
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def config_block(config, context)
|
|
413
|
+
device = config["device"] || config.dig("sys", "device") || {}
|
|
414
|
+
sys = config["sys"] || {}
|
|
415
|
+
lines = ["Config #{context[:host]}".strip, ""]
|
|
416
|
+
lines.concat(key_values([
|
|
417
|
+
["Name", device["name"] || config["name"]],
|
|
418
|
+
["MAC", device["mac"] || config["mac"]],
|
|
419
|
+
["Discoverable", device.key?("discoverable") ? device["discoverable"] : config["discoverable"]],
|
|
420
|
+
["Firmware", device["fw_id"] || config["fw"]],
|
|
421
|
+
["Cloud", enabled_label(config["cloud"])],
|
|
422
|
+
["MQTT", mqtt_config_label(config["mqtt"])],
|
|
423
|
+
["Config Rev", sys["cfg_rev"] || config["cfg_rev"]]
|
|
424
|
+
]))
|
|
425
|
+
wifi_config_summary(config["wifi"] || config).tap { |summary| lines.concat(["", summary]) unless summary.empty? }
|
|
426
|
+
component_config_summary(config).tap { |summary| lines.concat(["", summary]) unless summary.empty? }
|
|
427
|
+
lines.join("\n")
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def switch_line(value, context)
|
|
431
|
+
state = if value.key?("output") then value["output"] else value["ison"] end
|
|
432
|
+
power = value["apower"] || value["power"]
|
|
433
|
+
temp = temperature_value(value)
|
|
434
|
+
details = []
|
|
435
|
+
details << "power #{format_number(power)} W" if power
|
|
436
|
+
details << "temp #{format_number(temp)} C" if temp
|
|
437
|
+
"switch:#{context[:id]} #{on_off(state)}#{details.empty? ? "" : " (#{details.join(", ")})"}"
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def meter_block(value, context)
|
|
441
|
+
lines = ["Meter #{context[:host]} id #{context[:id]}"]
|
|
442
|
+
lines.concat(key_values([
|
|
443
|
+
["Power", value["power"] && "#{format_number(value["power"])} W"],
|
|
444
|
+
["Voltage", value["voltage"] && "#{format_number(value["voltage"])} V"],
|
|
445
|
+
["Current", value["current"] && "#{format_number(value["current"])} A"],
|
|
446
|
+
["Total", value["total"] || value["aenergy"]]
|
|
447
|
+
]))
|
|
448
|
+
lines.join("\n")
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def temperature_block(value, context)
|
|
452
|
+
lines = ["Temperature #{context[:host]} id #{context[:id]}"]
|
|
453
|
+
if value.values.all? { |entry| entry.is_a?(Hash) } && !value.key?("tC")
|
|
454
|
+
value.each do |id, entry|
|
|
455
|
+
lines << "#{id}: #{format_number(entry["tC"] || entry["temperature"])} C"
|
|
456
|
+
end
|
|
457
|
+
else
|
|
458
|
+
lines.concat(key_values([
|
|
459
|
+
["Temperature", (value["tC"] || value["temperature"]) && "#{format_number(value["tC"] || value["temperature"])} C"],
|
|
460
|
+
["Valid", value["is_valid"]]
|
|
461
|
+
]))
|
|
462
|
+
end
|
|
463
|
+
lines.join("\n")
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def component_block(value, context)
|
|
467
|
+
lines = ["#{context[:action].to_s.capitalize} #{context[:host]} id #{context[:id]}"]
|
|
468
|
+
lines.concat(key_values(value.map { |key, val| [key, scalar?(val) ? val : nil] }))
|
|
469
|
+
nested = value.reject { |_, val| scalar?(val) }
|
|
470
|
+
lines.concat(["", render_tree(nested)]) unless nested.empty?
|
|
471
|
+
lines.join("\n")
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def update_check_block(value, context)
|
|
475
|
+
state = value["has_update"] ? "update available" : "up to date"
|
|
476
|
+
line = "#{context[:host]}: #{state}"
|
|
477
|
+
current = value["current_version"]
|
|
478
|
+
new_version = value["new_version"]
|
|
479
|
+
[line, current && "Current: #{current}", new_version && "New: #{new_version}"].compact.join("\n")
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def write_line(value, context)
|
|
483
|
+
suffix = value.is_a?(Hash) && !value.empty? ? " #{compact_inline(value)}" : ""
|
|
484
|
+
"#{context[:action]}: #{context[:host]}#{suffix}"
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def input_setup_block(value, context)
|
|
488
|
+
"Input setup complete: #{context[:host]} device #{context[:device_id]} (#{Array(value).size} webhooks)"
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def raw_block(value, title = nil)
|
|
492
|
+
lines = []
|
|
493
|
+
lines << title if title
|
|
494
|
+
lines << render_tree(value)
|
|
495
|
+
lines.join("\n")
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def table(headers, rows)
|
|
499
|
+
rows = rows.map { |row| row.map { |value| display(value) } }
|
|
500
|
+
widths = headers.each_with_index.map do |header, index|
|
|
501
|
+
([header.length] + rows.map { |row| row[index].to_s.length }).max
|
|
502
|
+
end
|
|
503
|
+
lines = []
|
|
504
|
+
lines << headers.each_with_index.map { |header, index| header.ljust(widths[index]) }.join(" ")
|
|
505
|
+
lines << widths.map { |width| "-" * width }.join(" ")
|
|
506
|
+
rows.each do |row|
|
|
507
|
+
lines << row.each_with_index.map { |value, index| value.ljust(widths[index]) }.join(" ")
|
|
508
|
+
end
|
|
509
|
+
lines.join("\n")
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def key_values(pairs)
|
|
513
|
+
pairs = pairs.reject { |_, value| blank?(value) }
|
|
514
|
+
width = pairs.map { |key, _| key.length }.max || 0
|
|
515
|
+
pairs.map { |key, value| "#{key.ljust(width)}: #{display(value)}" }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def render_tree(value, indent = 0)
|
|
519
|
+
case value
|
|
520
|
+
when Hash
|
|
521
|
+
return "#{" " * indent}{}" if value.empty?
|
|
522
|
+
|
|
523
|
+
value.map do |key, child|
|
|
524
|
+
prefix = "#{" " * indent}#{key}:"
|
|
525
|
+
if scalar?(child) || child.nil?
|
|
526
|
+
"#{prefix} #{display(child)}"
|
|
527
|
+
else
|
|
528
|
+
"#{prefix}\n#{render_tree(child, indent + 1)}"
|
|
529
|
+
end
|
|
530
|
+
end.join("\n")
|
|
531
|
+
when Array
|
|
532
|
+
return "#{" " * indent}[]" if value.empty?
|
|
533
|
+
|
|
534
|
+
value.map do |child|
|
|
535
|
+
prefix = "#{" " * indent}-"
|
|
536
|
+
if scalar?(child) || child.nil?
|
|
537
|
+
"#{prefix} #{display(child)}"
|
|
538
|
+
else
|
|
539
|
+
"#{prefix}\n#{render_tree(child, indent + 1)}"
|
|
540
|
+
end
|
|
541
|
+
end.join("\n")
|
|
542
|
+
else
|
|
543
|
+
"#{" " * indent}#{display(value)}"
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def component_summary(hash)
|
|
548
|
+
lines = []
|
|
549
|
+
component_keys(hash).each do |key|
|
|
550
|
+
entry = hash[key] || {}
|
|
551
|
+
label = key.tr(":", " ")
|
|
552
|
+
values = []
|
|
553
|
+
values << on_off(entry["output"]) if entry.key?("output")
|
|
554
|
+
values << "input #{on_off(entry["state"])}" if entry.key?("state")
|
|
555
|
+
values << "#{format_number(entry["apower"] || entry["power"])} W" if entry["apower"] || entry["power"]
|
|
556
|
+
temp = temperature_value(entry)
|
|
557
|
+
values << "#{format_number(temp)} C" if temp
|
|
558
|
+
values << "source #{entry["source"]}" if entry["source"]
|
|
559
|
+
lines << "#{label}: #{values.empty? ? compact_inline(entry) : values.join(", ")}"
|
|
560
|
+
end
|
|
561
|
+
lines.join("\n")
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def component_config_summary(hash)
|
|
565
|
+
lines = []
|
|
566
|
+
component_keys(hash).each do |key|
|
|
567
|
+
entry = hash[key] || {}
|
|
568
|
+
bits = []
|
|
569
|
+
bits << "name #{entry["name"]}" if entry["name"]
|
|
570
|
+
bits << "type #{entry["type"]}" if entry["type"]
|
|
571
|
+
bits << "enabled #{display(entry["enable"])}" if entry.key?("enable")
|
|
572
|
+
bits << "mode #{entry["in_mode"]}" if entry["in_mode"]
|
|
573
|
+
bits << "initial #{entry["initial_state"]}" if entry["initial_state"]
|
|
574
|
+
bits << "auto_off #{entry["auto_off_delay"]}s" if entry["auto_off"]
|
|
575
|
+
lines << "#{key.tr(":", " ")}: #{bits.empty? ? compact_inline(entry) : bits.join(", ")}"
|
|
576
|
+
end
|
|
577
|
+
lines.join("\n")
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def component_keys(hash)
|
|
581
|
+
hash.keys.grep(/\A(?:switch|input|light|rgbw|temperature|humidity):\d+\z/).sort
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def temperature_value(hash)
|
|
585
|
+
value = hash["temperature"]
|
|
586
|
+
value.is_a?(Hash) ? value["tC"] : value
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def wifi_summary(wifi, raw = {})
|
|
590
|
+
ssid = wifi["ssid"] || raw.dig("wifi_sta", "ssid")
|
|
591
|
+
ip = wifi["sta_ip"] || wifi.dig("sta", "ip") || raw.dig("wifi_sta", "ip")
|
|
592
|
+
bssid = wifi["bssid"]
|
|
593
|
+
rssi = wifi["rssi"] || raw.dig("wifi_sta", "rssi")
|
|
594
|
+
status = wifi["status"] || (raw.dig("wifi_sta", "connected") == true ? "connected" : nil)
|
|
595
|
+
[ssid, ip, bssid, rssi && "#{rssi} dBm", status].compact.join(", ")
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def wifi_config_summary(wifi)
|
|
599
|
+
rows = []
|
|
600
|
+
if wifi["sta"] || wifi["sta1"]
|
|
601
|
+
%w[sta sta1].each do |slot|
|
|
602
|
+
sta = wifi[slot] || {}
|
|
603
|
+
rows << [slot, sta["enable"], sta["ssid"], sta["ipv4mode"], sta["ip"], sta["gw"], sta["nameserver"]]
|
|
604
|
+
end
|
|
605
|
+
elsif wifi["wifi_sta"] || wifi["wifi_sta1"]
|
|
606
|
+
%w[wifi_sta wifi_sta1].each do |slot|
|
|
607
|
+
sta = wifi[slot] || {}
|
|
608
|
+
rows << [slot, sta["enabled"], sta["ssid"], sta["ipv4_method"], sta["ip"], sta["gw"], sta["dns"]]
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
return "" if rows.empty?
|
|
612
|
+
|
|
613
|
+
"Wi-Fi\n" + table(%w[Slot Enabled SSID Mode IP GW DNS], rows).lines.map { |line| " #{line}" }.join
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def model_label(device)
|
|
617
|
+
model = device["model"]
|
|
618
|
+
model_id = device["model_id"]
|
|
619
|
+
model && model_id && model != model_id ? "#{model} (#{model_id})" : (model || model_id)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def update_label(update)
|
|
623
|
+
return nil unless update
|
|
624
|
+
return "available #{short_version(update["current_version"])} -> #{short_version(update["new_version"])}" if update["has_update"]
|
|
625
|
+
|
|
626
|
+
current = update["current_version"]
|
|
627
|
+
current ? "ok #{short_version(current)}" : "ok"
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def connection_label(hash)
|
|
631
|
+
return nil unless hash
|
|
632
|
+
connected = hash["connected"]
|
|
633
|
+
enabled = hash["enable"] || hash["enabled"]
|
|
634
|
+
if connected.nil?
|
|
635
|
+
enabled.nil? ? nil : enabled_label(hash)
|
|
636
|
+
else
|
|
637
|
+
connected ? "connected" : "disconnected"
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def enabled_label(hash)
|
|
642
|
+
return nil unless hash
|
|
643
|
+
value = hash["enable"]
|
|
644
|
+
value = hash["enabled"] if value.nil?
|
|
645
|
+
value.nil? ? nil : (value ? "enabled" : "disabled")
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def mqtt_config_label(hash)
|
|
649
|
+
return nil unless hash
|
|
650
|
+
label = enabled_label(hash)
|
|
651
|
+
server = hash["server"]
|
|
652
|
+
[label, server].compact.join(" ")
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def compact_inline(hash)
|
|
656
|
+
return display(hash) unless hash.is_a?(Hash)
|
|
657
|
+
|
|
658
|
+
hash.select { |_, value| scalar?(value) || value.nil? }
|
|
659
|
+
.map { |key, value| "#{key}=#{display(value)}" }
|
|
660
|
+
.join(", ")
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def on_off(value)
|
|
664
|
+
case value
|
|
665
|
+
when true then "on"
|
|
666
|
+
when false then "off"
|
|
667
|
+
when nil then "unknown"
|
|
668
|
+
else value.to_s
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def duration(seconds)
|
|
673
|
+
return nil unless seconds
|
|
674
|
+
|
|
675
|
+
seconds = seconds.to_i
|
|
676
|
+
days, rem = seconds.divmod(86_400)
|
|
677
|
+
hours, rem = rem.divmod(3600)
|
|
678
|
+
minutes, = rem.divmod(60)
|
|
679
|
+
parts = []
|
|
680
|
+
parts << "#{days}d" if days.positive?
|
|
681
|
+
parts << "#{hours}h" if hours.positive? || parts.any?
|
|
682
|
+
parts << "#{minutes}m"
|
|
683
|
+
parts.join(" ")
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def short_version(value)
|
|
687
|
+
value.to_s.split("/").last unless blank?(value)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def format_number(value)
|
|
691
|
+
return nil if value.nil?
|
|
692
|
+
number = Float(value)
|
|
693
|
+
number == number.to_i ? number.to_i.to_s : format("%.2f", number).sub(/0+\z/, "").sub(/\.\z/, "")
|
|
694
|
+
rescue ArgumentError, TypeError
|
|
695
|
+
value.to_s
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def display(value)
|
|
699
|
+
case value
|
|
700
|
+
when nil then "-"
|
|
701
|
+
when true then "true"
|
|
702
|
+
when false then "false"
|
|
703
|
+
when Array then value.join(", ")
|
|
704
|
+
else value.to_s.empty? ? "-" : value.to_s
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def scalar?(value)
|
|
709
|
+
value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def blank?(value)
|
|
713
|
+
value.nil? || value == "" || value == []
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def client_options
|
|
717
|
+
{ timeout: @options[:timeout] }
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def consume_flag!(name)
|
|
721
|
+
index = @argv.index(name)
|
|
722
|
+
return false unless index
|
|
723
|
+
|
|
724
|
+
@argv.delete_at(index)
|
|
725
|
+
true
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def consume_help_flag!
|
|
729
|
+
consume_flag!("--help") || consume_flag!("-h")
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def consume_option!(name, type = nil)
|
|
733
|
+
index = @argv.index(name)
|
|
734
|
+
return nil unless index
|
|
735
|
+
|
|
736
|
+
@argv.delete_at(index)
|
|
737
|
+
value = @argv.delete_at(index)
|
|
738
|
+
raise Error, "#{name} requires a value" unless value
|
|
739
|
+
|
|
740
|
+
type ? convert_option_value(type, value) : value
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def convert_option_value(type, value)
|
|
744
|
+
case type.name
|
|
745
|
+
when "Integer" then Integer(value)
|
|
746
|
+
when "Float" then Float(value)
|
|
747
|
+
when "String" then value
|
|
748
|
+
else type.call(value)
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def require_arg!(name)
|
|
753
|
+
value = @argv.shift
|
|
754
|
+
raise Error, "missing #{name}" unless value
|
|
755
|
+
|
|
756
|
+
value
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def parse_json_or_file(value)
|
|
760
|
+
json = File.exist?(value) ? File.read(value) : value
|
|
761
|
+
JSON.parse(json)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def parse_key_values(values)
|
|
765
|
+
values.each_with_object({}) do |pair, params|
|
|
766
|
+
key, value = pair.split("=", 2)
|
|
767
|
+
raise Error, "expected key=value, got #{pair.inspect}" unless key && value
|
|
768
|
+
|
|
769
|
+
params[key] = value
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def help
|
|
774
|
+
@stdout.puts <<~USAGE
|
|
775
|
+
Usage:
|
|
776
|
+
shelly [--json] [--timeout SECONDS] COMMAND ...
|
|
777
|
+
shelly help [COMMAND]
|
|
778
|
+
shelly COMMAND --help
|
|
779
|
+
|
|
780
|
+
Discovery and inspection:
|
|
781
|
+
shelly discover [--verify] [--concurrency N]
|
|
782
|
+
shelly scan [CIDR] [--concurrency N] [--max-hosts N]
|
|
783
|
+
shelly info HOST
|
|
784
|
+
shelly status HOST
|
|
785
|
+
shelly config HOST
|
|
786
|
+
|
|
787
|
+
Device control:
|
|
788
|
+
shelly switch HOST get|on|off|toggle [--id ID]
|
|
789
|
+
shelly meter HOST [--id ID]
|
|
790
|
+
shelly temp HOST [--id ID]
|
|
791
|
+
shelly light HOST get [--id ID]
|
|
792
|
+
shelly light HOST set [--id ID] [--on|--off] [--brightness N] [--transition SECONDS]
|
|
793
|
+
shelly rgbw HOST get [--id ID]
|
|
794
|
+
shelly rgbw HOST set [--id ID] [--on|--off] [--white N] [--brightness N] [--red N] [--green N] [--blue N] [--transition SECONDS]
|
|
795
|
+
|
|
796
|
+
Management:
|
|
797
|
+
shelly reboot HOST
|
|
798
|
+
shelly wifi HOST primary|secondary SSID PASSWORD [--ip IP] [--netmask MASK] [--gw IP] [--dns IP]
|
|
799
|
+
shelly update check HOST
|
|
800
|
+
shelly update start HOST [--stage stable]
|
|
801
|
+
shelly mqtt HOST SERVER [--disable]
|
|
802
|
+
shelly input setup HOST DEVICE_ID [--base-url URL] [--count N]
|
|
803
|
+
shelly webhook delete-all HOST
|
|
804
|
+
|
|
805
|
+
Escape hatches:
|
|
806
|
+
shelly rpc HOST METHOD [JSON_OR_FILE]
|
|
807
|
+
shelly gen1 HOST PATH [key=value ...]
|
|
808
|
+
USAGE
|
|
809
|
+
0
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def command_help(command)
|
|
813
|
+
return help unless command
|
|
814
|
+
|
|
815
|
+
key = COMMAND_ALIASES.fetch(command, command)
|
|
816
|
+
text = command_help_text(key)
|
|
817
|
+
raise Error, "unknown help topic: #{command}" unless text
|
|
818
|
+
|
|
819
|
+
@stdout.puts text
|
|
820
|
+
0
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def command_help_text(command)
|
|
824
|
+
case command
|
|
825
|
+
when "discover"
|
|
826
|
+
<<~HELP
|
|
827
|
+
Usage:
|
|
828
|
+
shelly discover [--verify] [--concurrency N]
|
|
829
|
+
|
|
830
|
+
Find Shelly devices via mDNS.
|
|
831
|
+
|
|
832
|
+
Options:
|
|
833
|
+
--verify Connect to each discovered IPv4 device and print normalized device info.
|
|
834
|
+
--concurrency N Number of parallel HTTP verification requests with --verify. Default: 32.
|
|
835
|
+
|
|
836
|
+
Notes:
|
|
837
|
+
Uses avahi-browse on Linux and dns-sd on macOS. This depends on working mDNS.
|
|
838
|
+
HELP
|
|
839
|
+
when "scan"
|
|
840
|
+
<<~HELP
|
|
841
|
+
Usage:
|
|
842
|
+
shelly scan [CIDR] [--concurrency N] [--max-hosts N]
|
|
843
|
+
|
|
844
|
+
Probe IPv4 addresses for Shelly devices without using mDNS.
|
|
845
|
+
|
|
846
|
+
Arguments:
|
|
847
|
+
CIDR Optional network, for example 192.168.0.0/24. If omitted, the current IPv4 network is used.
|
|
848
|
+
|
|
849
|
+
Options:
|
|
850
|
+
--concurrency N Number of parallel HTTP probes. Default: 32.
|
|
851
|
+
--max-hosts N Safety cap for addresses to scan. Default: 4096.
|
|
852
|
+
|
|
853
|
+
Examples:
|
|
854
|
+
shelly scan
|
|
855
|
+
shelly --timeout 2 scan 192.168.0.0/24 --concurrency 64
|
|
856
|
+
HELP
|
|
857
|
+
when "info"
|
|
858
|
+
<<~HELP
|
|
859
|
+
Usage:
|
|
860
|
+
shelly info HOST
|
|
861
|
+
|
|
862
|
+
Print normalized device information: generation, model, MAC, name, firmware, update state, and raw merged data.
|
|
863
|
+
|
|
864
|
+
Example:
|
|
865
|
+
shelly --json info 192.168.0.55
|
|
866
|
+
HELP
|
|
867
|
+
when "status"
|
|
868
|
+
<<~HELP
|
|
869
|
+
Usage:
|
|
870
|
+
shelly status HOST
|
|
871
|
+
|
|
872
|
+
Print current device status. Uses /status on Gen1 and Shelly.GetStatus on Gen2+.
|
|
873
|
+
HELP
|
|
874
|
+
when "config"
|
|
875
|
+
<<~HELP
|
|
876
|
+
Usage:
|
|
877
|
+
shelly config HOST
|
|
878
|
+
|
|
879
|
+
Print current device configuration. Uses /settings on Gen1 and Shelly.GetConfig on Gen2+.
|
|
880
|
+
HELP
|
|
881
|
+
when "switch"
|
|
882
|
+
<<~HELP
|
|
883
|
+
Usage:
|
|
884
|
+
shelly switch HOST get|on|off|toggle [--id ID]
|
|
885
|
+
|
|
886
|
+
Read or control a switch/relay output.
|
|
887
|
+
|
|
888
|
+
Options:
|
|
889
|
+
--id ID Output id. Default: 0.
|
|
890
|
+
|
|
891
|
+
Examples:
|
|
892
|
+
shelly switch 192.168.0.71 get
|
|
893
|
+
shelly switch 192.168.0.71 off --id 0
|
|
894
|
+
HELP
|
|
895
|
+
when "meter"
|
|
896
|
+
<<~HELP
|
|
897
|
+
Usage:
|
|
898
|
+
shelly meter HOST [--id ID]
|
|
899
|
+
|
|
900
|
+
Print power meter data. On Gen2+ this reads Switch.GetStatus for the given id.
|
|
901
|
+
|
|
902
|
+
Options:
|
|
903
|
+
--id ID Meter or switch id. Default: 0.
|
|
904
|
+
HELP
|
|
905
|
+
when "temp"
|
|
906
|
+
<<~HELP
|
|
907
|
+
Usage:
|
|
908
|
+
shelly temp HOST [--id ID]
|
|
909
|
+
shelly temperature HOST [--id ID]
|
|
910
|
+
|
|
911
|
+
Print temperature sensor data.
|
|
912
|
+
|
|
913
|
+
Options:
|
|
914
|
+
--id ID Gen2+ temperature component id. Default: 100.
|
|
915
|
+
HELP
|
|
916
|
+
when "light"
|
|
917
|
+
<<~HELP
|
|
918
|
+
Usage:
|
|
919
|
+
shelly light HOST get [--id ID]
|
|
920
|
+
shelly light HOST set [--id ID] [--on|--off] [--brightness N] [--transition SECONDS]
|
|
921
|
+
|
|
922
|
+
Read or control a Gen2+ Light component.
|
|
923
|
+
|
|
924
|
+
Options:
|
|
925
|
+
--id ID Light id. Default: 0.
|
|
926
|
+
--on, --off Desired output state.
|
|
927
|
+
--brightness N Brightness value accepted by the device.
|
|
928
|
+
--transition SECONDS Transition duration.
|
|
929
|
+
HELP
|
|
930
|
+
when "rgbw"
|
|
931
|
+
<<~HELP
|
|
932
|
+
Usage:
|
|
933
|
+
shelly rgbw HOST get [--id ID]
|
|
934
|
+
shelly rgbw HOST set [--id ID] [--on|--off] [--white N] [--brightness N] [--red N] [--green N] [--blue N] [--transition SECONDS]
|
|
935
|
+
|
|
936
|
+
Read or control a Gen2+ RGBW component.
|
|
937
|
+
|
|
938
|
+
Options:
|
|
939
|
+
--id ID RGBW id. Default: 0.
|
|
940
|
+
--on, --off Desired output state.
|
|
941
|
+
--white N White channel value.
|
|
942
|
+
--brightness N Brightness value accepted by the device.
|
|
943
|
+
--red N --green N --blue N
|
|
944
|
+
--transition SECONDS Transition duration.
|
|
945
|
+
HELP
|
|
946
|
+
when "reboot"
|
|
947
|
+
<<~HELP
|
|
948
|
+
Usage:
|
|
949
|
+
shelly reboot HOST
|
|
950
|
+
|
|
951
|
+
Reboot a device. Uses /reboot on Gen1 and Shelly.Reboot on Gen2+.
|
|
952
|
+
HELP
|
|
953
|
+
when "wifi"
|
|
954
|
+
<<~HELP
|
|
955
|
+
Usage:
|
|
956
|
+
shelly wifi HOST primary|secondary SSID PASSWORD [--ip IP] [--netmask MASK] [--gw IP] [--dns IP]
|
|
957
|
+
|
|
958
|
+
Configure a Wi-Fi station slot.
|
|
959
|
+
|
|
960
|
+
Slot mapping:
|
|
961
|
+
primary Gen2+ WiFi sta, Gen1 /settings/sta.
|
|
962
|
+
secondary Gen2+ WiFi sta1, Gen1 /settings/sta1.
|
|
963
|
+
|
|
964
|
+
By default the station uses DHCP. If --ip is provided, static IPv4 is configured.
|
|
965
|
+
|
|
966
|
+
Static defaults:
|
|
967
|
+
--netmask Defaults to 255.255.255.0.
|
|
968
|
+
--gw Defaults to .1 in the same /24 as --ip.
|
|
969
|
+
--dns Defaults to the gateway.
|
|
970
|
+
|
|
971
|
+
Examples:
|
|
972
|
+
shelly wifi 192.168.0.55 secondary NewSSID 'new password'
|
|
973
|
+
shelly wifi 192.168.0.55 primary NewSSID 'new password' --ip 192.168.10.55
|
|
974
|
+
HELP
|
|
975
|
+
when "update"
|
|
976
|
+
<<~HELP
|
|
977
|
+
Usage:
|
|
978
|
+
shelly update check HOST
|
|
979
|
+
shelly update start HOST [--stage stable|beta]
|
|
980
|
+
|
|
981
|
+
Check for firmware updates or start an update.
|
|
982
|
+
|
|
983
|
+
Options:
|
|
984
|
+
--stage NAME Gen2+ update stage for start. Default: stable.
|
|
985
|
+
HELP
|
|
986
|
+
when "mqtt"
|
|
987
|
+
<<~HELP
|
|
988
|
+
Usage:
|
|
989
|
+
shelly mqtt HOST SERVER [--disable]
|
|
990
|
+
|
|
991
|
+
Enable or disable MQTT and set the MQTT server.
|
|
992
|
+
|
|
993
|
+
Examples:
|
|
994
|
+
shelly mqtt 192.168.0.55 192.168.0.10:1883
|
|
995
|
+
shelly mqtt 192.168.0.55 192.168.0.10:1883 --disable
|
|
996
|
+
HELP
|
|
997
|
+
when "input"
|
|
998
|
+
<<~HELP
|
|
999
|
+
Usage:
|
|
1000
|
+
shelly input setup HOST DEVICE_ID [--base-url URL] [--count N]
|
|
1001
|
+
|
|
1002
|
+
Configure a Gen2+ input device for the home.home input webhook workflow.
|
|
1003
|
+
|
|
1004
|
+
Actions:
|
|
1005
|
+
setup Set device/input names, delete existing webhooks, and create push, longpush, and doublepush webhooks.
|
|
1006
|
+
|
|
1007
|
+
Options:
|
|
1008
|
+
--base-url URL Base webhook URL. Default: http://home.home/inputs.
|
|
1009
|
+
--count N Number of inputs to configure. Default: 4.
|
|
1010
|
+
--device-id ID Alternative way to provide DEVICE_ID.
|
|
1011
|
+
HELP
|
|
1012
|
+
when "webhook"
|
|
1013
|
+
<<~HELP
|
|
1014
|
+
Usage:
|
|
1015
|
+
shelly webhook delete-all HOST
|
|
1016
|
+
|
|
1017
|
+
Delete all Gen2+ webhooks from a device.
|
|
1018
|
+
HELP
|
|
1019
|
+
when "rpc"
|
|
1020
|
+
<<~HELP
|
|
1021
|
+
Usage:
|
|
1022
|
+
shelly rpc HOST METHOD [JSON_OR_FILE]
|
|
1023
|
+
|
|
1024
|
+
Call a raw Gen2+ RPC method. The optional params argument can be an inline JSON object or a path to a JSON file.
|
|
1025
|
+
|
|
1026
|
+
Examples:
|
|
1027
|
+
shelly rpc 192.168.0.55 Shelly.GetStatus
|
|
1028
|
+
shelly rpc 192.168.0.55 Switch.Set '{"id":0,"on":false}'
|
|
1029
|
+
HELP
|
|
1030
|
+
when "gen1"
|
|
1031
|
+
<<~HELP
|
|
1032
|
+
Usage:
|
|
1033
|
+
shelly gen1 HOST PATH [key=value ...]
|
|
1034
|
+
shelly get HOST PATH [key=value ...]
|
|
1035
|
+
|
|
1036
|
+
Call a raw Gen1 HTTP JSON endpoint.
|
|
1037
|
+
|
|
1038
|
+
Examples:
|
|
1039
|
+
shelly gen1 192.168.0.51 /settings
|
|
1040
|
+
shelly gen1 192.168.0.51 /relay/0 turn=off
|
|
1041
|
+
HELP
|
|
1042
|
+
when "help"
|
|
1043
|
+
<<~HELP
|
|
1044
|
+
Usage:
|
|
1045
|
+
shelly help [COMMAND]
|
|
1046
|
+
shelly COMMAND --help
|
|
1047
|
+
|
|
1048
|
+
Print the global command summary or detailed help for a specific command.
|
|
1049
|
+
|
|
1050
|
+
Examples:
|
|
1051
|
+
shelly help
|
|
1052
|
+
shelly help wifi
|
|
1053
|
+
shelly scan --help
|
|
1054
|
+
HELP
|
|
1055
|
+
when "version"
|
|
1056
|
+
<<~HELP
|
|
1057
|
+
Usage:
|
|
1058
|
+
shelly version
|
|
1059
|
+
shelly --version
|
|
1060
|
+
shelly -v
|
|
1061
|
+
|
|
1062
|
+
Print the rshelly gem version.
|
|
1063
|
+
HELP
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
end
|
|
1067
|
+
end
|