esphome 0.6.0 → 1.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 +4 -4
- data/exe/esphome-monitor +16 -14
- data/exe/esphome-serial-proxy +132 -0
- data/lib/esphome/action.rb +55 -0
- data/lib/esphome/api/README.md +3 -0
- data/lib/esphome/api/api.proto +2756 -0
- data/lib/esphome/api/api_options.proto +120 -0
- data/lib/esphome/api/api_options_pb.rb +20 -0
- data/lib/esphome/api/api_pb.rb +229 -0
- data/lib/esphome/api.rb +182 -0
- data/lib/esphome/cli/colors.rb +44 -0
- data/lib/esphome/cli/completion.rb +48 -0
- data/lib/esphome/cli/entities/button.rb +18 -0
- data/lib/esphome/cli/entities/climate.rb +196 -0
- data/lib/esphome/cli/entities/cover.rb +116 -0
- data/lib/esphome/cli/entities/date.rb +27 -0
- data/lib/esphome/cli/entities/date_time.rb +29 -0
- data/lib/esphome/cli/entities/form.rb +111 -0
- data/lib/esphome/cli/entities/lock.rb +20 -0
- data/lib/esphome/cli/entities/menu.rb +83 -0
- data/lib/esphome/cli/entities/number.rb +37 -0
- data/lib/esphome/cli/entities/select.rb +16 -0
- data/lib/esphome/cli/entities/subfields.rb +63 -0
- data/lib/esphome/cli/entities/switch.rb +18 -0
- data/lib/esphome/cli/entities/text.rb +16 -0
- data/lib/esphome/cli/entities/time.rb +40 -0
- data/lib/esphome/cli/entity.rb +78 -0
- data/lib/esphome/cli/monitor.rb +441 -0
- data/lib/esphome/dashboard.rb +86 -0
- data/lib/esphome/device.rb +416 -0
- data/lib/esphome/entities/binary_sensor.rb +66 -0
- data/lib/esphome/entities/button.rb +13 -0
- data/lib/esphome/entities/climate.rb +259 -0
- data/lib/esphome/entities/cover.rb +135 -0
- data/lib/esphome/entities/date.rb +23 -0
- data/lib/esphome/entities/date_time.rb +21 -0
- data/lib/esphome/entities/fan.rb +89 -0
- data/lib/esphome/entities/light.rb +158 -0
- data/lib/esphome/entities/lock.rb +53 -0
- data/lib/esphome/entities/number.rb +56 -0
- data/lib/esphome/entities/select.rb +27 -0
- data/lib/esphome/entities/sensor.rb +49 -0
- data/lib/esphome/entities/switch.rb +24 -0
- data/lib/esphome/entities/text.rb +36 -0
- data/lib/esphome/entities/text_sensor.rb +10 -0
- data/lib/esphome/entities/time.rb +39 -0
- data/lib/esphome/entity.rb +146 -0
- data/lib/esphome/error.rb +28 -0
- data/lib/esphome/serial_proxy.rb +372 -0
- data/lib/esphome/version.rb +5 -0
- data/lib/esphome.rb +5 -0
- data/lib/httpx/plugins/websocket.rb +62 -0
- data/lib/websocket/driver/httpx.rb +74 -0
- metadata +55 -5
- data/exe/esphome-update-all +0 -66
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ENV["ESCDELAY"] = "0"
|
|
4
|
+
require "curses"
|
|
5
|
+
require "io/console"
|
|
6
|
+
|
|
7
|
+
require "esphome"
|
|
8
|
+
|
|
9
|
+
require_relative "colors"
|
|
10
|
+
require_relative "entity"
|
|
11
|
+
|
|
12
|
+
module ESPHome
|
|
13
|
+
module Cli
|
|
14
|
+
class Monitor
|
|
15
|
+
include Colors
|
|
16
|
+
|
|
17
|
+
HEADER_ROWS = 3
|
|
18
|
+
|
|
19
|
+
class LoggerWrapper < Logger
|
|
20
|
+
def initialize(parent, level: Logger::DEBUG)
|
|
21
|
+
super(IO::NULL, level:)
|
|
22
|
+
|
|
23
|
+
@parent = parent
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add(severity, message = nil, progname = nil, ...)
|
|
27
|
+
severity ||= UNKNOWN
|
|
28
|
+
return true if severity < level
|
|
29
|
+
|
|
30
|
+
@parent.add(severity, message, progname, ...)
|
|
31
|
+
end
|
|
32
|
+
alias_method :log, :add
|
|
33
|
+
end
|
|
34
|
+
private_constant :LoggerWrapper
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def run(...)
|
|
38
|
+
new(...).run
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :name_width, :win
|
|
43
|
+
attr_accessor :logger
|
|
44
|
+
|
|
45
|
+
def initialize(device,
|
|
46
|
+
actions: false,
|
|
47
|
+
device_log_level: nil,
|
|
48
|
+
connection_log_level: nil,
|
|
49
|
+
log_level: nil,
|
|
50
|
+
dump_config: false)
|
|
51
|
+
@device = device
|
|
52
|
+
@win = nil
|
|
53
|
+
@entities_by_key = {}
|
|
54
|
+
@entities = []
|
|
55
|
+
@log_lines = []
|
|
56
|
+
@current_entity = -1
|
|
57
|
+
@sub_active = false
|
|
58
|
+
@name_width = 0
|
|
59
|
+
@device.device_logger = LoggerWrapper.new(self, level: device_log_level || Logger::DEBUG)
|
|
60
|
+
@device.connection_logger = LoggerWrapper.new(self, level: connection_log_level || Logger::WARN)
|
|
61
|
+
@logger = LoggerWrapper.new(self, level: log_level || Logger::INFO)
|
|
62
|
+
@actions = actions
|
|
63
|
+
@dump_config = dump_config
|
|
64
|
+
@winch_trapped = false
|
|
65
|
+
@logwin = nil
|
|
66
|
+
@screen_initialized = false
|
|
67
|
+
@resize_pending = false
|
|
68
|
+
@ui_events = Queue.new
|
|
69
|
+
@log_mutex = Mutex.new
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
Logger::Severity.constants.each do |level|
|
|
73
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
74
|
+
def #{level.downcase}(msg = nil, ...) # def warn(msg = nil, ...)
|
|
75
|
+
add(Logger::#{level}, msg, ...) # add(Logger::WARN, msg, ...)
|
|
76
|
+
end # end
|
|
77
|
+
RUBY
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run
|
|
81
|
+
@device.on_message do |entity_or_log_line|
|
|
82
|
+
if entity_or_log_line.respond_to?(:key) && (entity_wrapper = @entities_by_key[entity_or_log_line.key])
|
|
83
|
+
entity_wrapper.touch
|
|
84
|
+
queue_ui_event(:entity, entity_wrapper)
|
|
85
|
+
elsif entity_or_log_line.is_a?(Action)
|
|
86
|
+
logger.info(entity_or_log_line.inspect)
|
|
87
|
+
else
|
|
88
|
+
logger.warn("Unexpected message #{entity_or_log_line.inspect}")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@device.on_connect do
|
|
93
|
+
logger.info("Connected")
|
|
94
|
+
@name_width = @device.entities.values.map { |e| e.name.length }.max || 0
|
|
95
|
+
@entities_by_key = {}
|
|
96
|
+
@entities = []
|
|
97
|
+
@device.entities.values.sort_by(&:name).each_with_index do |entity, idx|
|
|
98
|
+
simple_name = entity.class.name.split("::").last
|
|
99
|
+
entity_class = if Entities.const_defined?(simple_name)
|
|
100
|
+
Entities.const_get(simple_name, false)
|
|
101
|
+
else
|
|
102
|
+
Entity
|
|
103
|
+
end
|
|
104
|
+
entity_wrapper = entity_class.new(self, entity, idx)
|
|
105
|
+
@entities_by_key[entity.key] = entity_wrapper
|
|
106
|
+
@entities << entity_wrapper
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@current_entity = @entities.index { |e| e.respond_to?(:activate) } || -1
|
|
110
|
+
queue_ui_event(:render_all)
|
|
111
|
+
|
|
112
|
+
@device.stream_states
|
|
113
|
+
@device.stream_actions if @actions
|
|
114
|
+
@device.stream_log(dump_config: @dump_config) if @device.device_logger.level < Logger::FATAL
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@device.on_disconnect do
|
|
118
|
+
logger.info("Gracefully disconnected")
|
|
119
|
+
reconnect
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
install_winch_handler
|
|
123
|
+
@device.connect
|
|
124
|
+
ensure_screen
|
|
125
|
+
process_ui_events
|
|
126
|
+
|
|
127
|
+
begin
|
|
128
|
+
Thread.new do
|
|
129
|
+
@device.loop
|
|
130
|
+
rescue DeviceConnectionError, NotConnectedError, IOError, SocketError, SystemCallError, Timeout::Error => e
|
|
131
|
+
logger.warn("Connection lost: #{e}")
|
|
132
|
+
@device.disconnect
|
|
133
|
+
reconnect
|
|
134
|
+
retry
|
|
135
|
+
rescue => e
|
|
136
|
+
error("UNHANDLED EXCEPTION: #{e}", render: false)
|
|
137
|
+
e.backtrace.each do |line|
|
|
138
|
+
error(" #{line}", render: false)
|
|
139
|
+
end
|
|
140
|
+
queue_ui_event(:log)
|
|
141
|
+
retry
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
loop do
|
|
145
|
+
process_ui_events
|
|
146
|
+
|
|
147
|
+
case @win.getch
|
|
148
|
+
when Curses::Key::UP
|
|
149
|
+
new_entity = @entities.reverse.find { |e| e.index < @current_entity && e.respond_to?(:activate) }&.index
|
|
150
|
+
next unless new_entity
|
|
151
|
+
|
|
152
|
+
@entities[@current_entity]&.print(@win) if @current_entity >= 0
|
|
153
|
+
@current_entity = new_entity
|
|
154
|
+
@entities[@current_entity].print(@win, active: true)
|
|
155
|
+
@win.refresh
|
|
156
|
+
when Curses::Key::DOWN
|
|
157
|
+
new_entity = @entities.find { |e| e.index > @current_entity && e.respond_to?(:activate) }&.index
|
|
158
|
+
next unless new_entity
|
|
159
|
+
|
|
160
|
+
@entities[@current_entity]&.print(@win) if @current_entity >= 0
|
|
161
|
+
@current_entity = new_entity
|
|
162
|
+
@entities[@current_entity].print(@win, active: true)
|
|
163
|
+
@win.refresh
|
|
164
|
+
when Curses::Key::LEFT
|
|
165
|
+
next unless @current_entity >= 0
|
|
166
|
+
|
|
167
|
+
@entities[@current_entity].move_left
|
|
168
|
+
@entities[@current_entity].print(@win, active: true)
|
|
169
|
+
@win.refresh
|
|
170
|
+
when Curses::Key::RIGHT
|
|
171
|
+
next unless @current_entity >= 0
|
|
172
|
+
|
|
173
|
+
@entities[@current_entity].move_right
|
|
174
|
+
@entities[@current_entity].print(@win, active: true)
|
|
175
|
+
@win.refresh
|
|
176
|
+
when "\n".ord
|
|
177
|
+
entity_wrapper = @entities[@current_entity]
|
|
178
|
+
activate_entity(entity_wrapper) if entity_wrapper.respond_to?(:activate)
|
|
179
|
+
when 27 # Esc
|
|
180
|
+
break
|
|
181
|
+
when nil
|
|
182
|
+
next
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
rescue Interrupt
|
|
186
|
+
# exiting
|
|
187
|
+
ensure
|
|
188
|
+
close_screen
|
|
189
|
+
end
|
|
190
|
+
@device.disconnect
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def add(_level, message = nil, progname = nil, render: true)
|
|
194
|
+
if message.nil?
|
|
195
|
+
message = if block_given?
|
|
196
|
+
yield
|
|
197
|
+
else
|
|
198
|
+
progname
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
log_line = "[#{Time.now.strftime("%H:%M:%S.%L")}] #{message}"
|
|
203
|
+
@log_mutex.synchronize do
|
|
204
|
+
@log_lines << log_line
|
|
205
|
+
@log_lines.shift if @log_lines.size > [20, log_area_height].max
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
puts log_line unless @screen_initialized
|
|
209
|
+
queue_ui_event(:log) if render
|
|
210
|
+
end
|
|
211
|
+
alias_method :log, :add
|
|
212
|
+
|
|
213
|
+
def resize_pending?
|
|
214
|
+
@resize_pending
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def parse_ansi_and_render(line)
|
|
220
|
+
@logwin.clrtoeol
|
|
221
|
+
current_color = nil
|
|
222
|
+
current_attr = Curses::A_NORMAL
|
|
223
|
+
|
|
224
|
+
line.split(ANSI_ESCAPE_REGEX).each_with_index do |part, i|
|
|
225
|
+
if i.odd?
|
|
226
|
+
codes = part.split(";").map(&:to_i)
|
|
227
|
+
current_color, current_attr = parse_sgr_codes(codes)
|
|
228
|
+
elsif current_color
|
|
229
|
+
pair_id = current_color + 1
|
|
230
|
+
@logwin.attron(Curses.color_pair(pair_id) | current_attr) do
|
|
231
|
+
@logwin.addstr(part)
|
|
232
|
+
end
|
|
233
|
+
else
|
|
234
|
+
@logwin.attron(current_attr) do
|
|
235
|
+
@logwin.addstr(part)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
rescue Curses::Error
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def log_area_height
|
|
244
|
+
return 20 unless @win
|
|
245
|
+
|
|
246
|
+
[@win.maxy - @entities.size - 4, 0].max
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def render_log
|
|
250
|
+
return unless @logwin
|
|
251
|
+
|
|
252
|
+
@logwin.clear
|
|
253
|
+
@logwin.setpos(0, 0)
|
|
254
|
+
|
|
255
|
+
@log_mutex.synchronize do
|
|
256
|
+
@log_lines.each_with_index do |log_line, idx|
|
|
257
|
+
unless idx.zero?
|
|
258
|
+
if @logwin.cury == @logwin.maxy - 1
|
|
259
|
+
@logwin.scroll
|
|
260
|
+
@logwin.setpos(@logwin.cury, 0)
|
|
261
|
+
else
|
|
262
|
+
@logwin.setpos(@logwin.cury + 1, 0)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
parse_ansi_and_render(log_line)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
@logwin.refresh
|
|
271
|
+
rescue Curses::Error
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def rebuild_logwin
|
|
276
|
+
@logwin&.close
|
|
277
|
+
@logwin = nil
|
|
278
|
+
|
|
279
|
+
height = log_area_height
|
|
280
|
+
return if height <= 0
|
|
281
|
+
|
|
282
|
+
top = @entities.size + 4
|
|
283
|
+
return if top >= @win.maxy
|
|
284
|
+
|
|
285
|
+
@logwin = @win.subwin(height, @win.maxx, top, 0)
|
|
286
|
+
@logwin.scrollok(true)
|
|
287
|
+
rescue Curses::Error
|
|
288
|
+
@logwin = nil
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def render_all
|
|
292
|
+
ensure_screen
|
|
293
|
+
rebuild_logwin
|
|
294
|
+
@win.clear
|
|
295
|
+
|
|
296
|
+
safe_setpos(@win, 0, 0)
|
|
297
|
+
safe_addstr(@win, "ESPHome")
|
|
298
|
+
safe_setpos(@win, 1, 0)
|
|
299
|
+
left_header = @device.friendly_name.to_s
|
|
300
|
+
right_header = "#{@device.esphome_version} - #{@device.compilation_time}"
|
|
301
|
+
safe_addstr(@win, left_header)
|
|
302
|
+
if right_header.length < @win.maxx
|
|
303
|
+
safe_setpos(@win, 1, @win.maxx - right_header.length)
|
|
304
|
+
safe_addstr(@win, right_header)
|
|
305
|
+
end
|
|
306
|
+
safe_setpos(@win, 2, 0)
|
|
307
|
+
safe_addstr(@win, "=" * @win.maxx)
|
|
308
|
+
|
|
309
|
+
@entities.each do |entity|
|
|
310
|
+
entity.print(@win, active: @current_entity == entity.index)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
render_log
|
|
314
|
+
|
|
315
|
+
@win.refresh
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def ensure_screen
|
|
319
|
+
return if @screen_initialized && @win
|
|
320
|
+
|
|
321
|
+
@win = Curses.init_screen
|
|
322
|
+
Curses.noecho
|
|
323
|
+
Curses.start_color
|
|
324
|
+
Curses.use_default_colors
|
|
325
|
+
@win.keypad = true
|
|
326
|
+
@win.timeout = 100
|
|
327
|
+
Colors::ANSI_COLOR_MAP.each_value do |color|
|
|
328
|
+
Curses.init_pair(color + 1, color, -1)
|
|
329
|
+
end
|
|
330
|
+
@screen_initialized = true
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def close_screen
|
|
334
|
+
return unless @screen_initialized
|
|
335
|
+
|
|
336
|
+
@logwin&.close
|
|
337
|
+
@logwin = nil
|
|
338
|
+
Curses.close_screen
|
|
339
|
+
@win = nil
|
|
340
|
+
@screen_initialized = false
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def install_winch_handler
|
|
344
|
+
return if @winch_trapped
|
|
345
|
+
|
|
346
|
+
@winch_trapped = true
|
|
347
|
+
Signal.trap(:WINCH) do
|
|
348
|
+
@resize_pending = true
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def process_ui_events
|
|
353
|
+
if @resize_pending
|
|
354
|
+
if @screen_initialized
|
|
355
|
+
rows, cols = IO.console.winsize
|
|
356
|
+
Curses.resizeterm(rows, cols)
|
|
357
|
+
@win&.resize(rows, cols)
|
|
358
|
+
@win&.clear
|
|
359
|
+
else
|
|
360
|
+
close_screen
|
|
361
|
+
end
|
|
362
|
+
@resize_pending = false
|
|
363
|
+
queue_ui_event(:render_all)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
render_all_needed = false
|
|
367
|
+
log_dirty = false
|
|
368
|
+
entity_updates = []
|
|
369
|
+
|
|
370
|
+
loop do
|
|
371
|
+
type, payload = @ui_events.pop(true)
|
|
372
|
+
case type
|
|
373
|
+
when :entity
|
|
374
|
+
entity_updates << payload
|
|
375
|
+
when :log
|
|
376
|
+
log_dirty = true
|
|
377
|
+
when :render_all
|
|
378
|
+
render_all_needed = true
|
|
379
|
+
end
|
|
380
|
+
rescue ThreadError
|
|
381
|
+
break
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
return if @sub_active
|
|
385
|
+
|
|
386
|
+
if render_all_needed
|
|
387
|
+
render_all
|
|
388
|
+
return
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
return if entity_updates.empty? && !log_dirty
|
|
392
|
+
|
|
393
|
+
ensure_screen
|
|
394
|
+
entity_updates.uniq.each do |entity|
|
|
395
|
+
entity.print(@win, active: @current_entity == entity.index)
|
|
396
|
+
end
|
|
397
|
+
render_log if log_dirty
|
|
398
|
+
@win.refresh
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def queue_ui_event(type, payload = nil)
|
|
402
|
+
@ui_events << [type, payload]
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def safe_setpos(window, row, column)
|
|
406
|
+
return if row.negative? || row >= window.maxy || column.negative? || column >= window.maxx
|
|
407
|
+
|
|
408
|
+
window.setpos(row, column)
|
|
409
|
+
rescue Curses::Error
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def safe_addstr(window, string)
|
|
414
|
+
return if string.nil? || string.empty?
|
|
415
|
+
|
|
416
|
+
remaining = window.maxx - window.curx
|
|
417
|
+
return if remaining <= 0
|
|
418
|
+
|
|
419
|
+
window.addstr(string[0, remaining])
|
|
420
|
+
rescue Curses::Error
|
|
421
|
+
nil
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def reconnect
|
|
425
|
+
@device.connect
|
|
426
|
+
rescue DeviceConnectionError, NotConnectedError, IOError, SocketError, SystemCallError, Timeout::Error => e
|
|
427
|
+
logger.warn("Failed to reconnect: #{e}")
|
|
428
|
+
sleep 1
|
|
429
|
+
retry
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def activate_entity(entity_wrapper)
|
|
433
|
+
@sub_active = true
|
|
434
|
+
entity_wrapper.activate
|
|
435
|
+
queue_ui_event(:render_all)
|
|
436
|
+
ensure
|
|
437
|
+
@sub_active = false
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httpx"
|
|
4
|
+
require_relative "error"
|
|
5
|
+
|
|
6
|
+
module ESPHome
|
|
7
|
+
class Dashboard
|
|
8
|
+
DEFAULT_URI = "http://localhost:6052/"
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def address_and_encryption_key(uri, device_name)
|
|
12
|
+
dashboard = uri.is_a?(Dashboard) ? uri : new(uri)
|
|
13
|
+
device = dashboard.devices.find { |d| d["name"] == device_name }
|
|
14
|
+
raise NoSuchDeviceError, "Device not found: #{device_name}" unless device
|
|
15
|
+
|
|
16
|
+
address = device["address"]
|
|
17
|
+
encryption_key = dashboard.encryption_key(device["configuration"])
|
|
18
|
+
raise MissingEncryptionKeyError, "No encryption key found for device #{device_name}" unless encryption_key
|
|
19
|
+
|
|
20
|
+
[address, encryption_key]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(uri)
|
|
25
|
+
@http = HTTPX.plugin(:persistent).with(origin: uri)
|
|
26
|
+
@websocket = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def devices
|
|
30
|
+
@http.get("/devices").tap(&:raise_for_status).json["configured"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def config(configuration)
|
|
34
|
+
@http.get("/json-config", params: { configuration: })
|
|
35
|
+
.tap(&:raise_for_status)
|
|
36
|
+
.json(allow_nan: true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def encryption_key(configuration)
|
|
40
|
+
config(configuration).dig("api", "encryption", "key")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def compile(configuration, &)
|
|
44
|
+
websocket_command("/compile", { type: "spawn", configuration: }, &)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def upload(configuration, port: :ota, &)
|
|
48
|
+
port = "OTA" if port == :ota
|
|
49
|
+
websocket_command("/upload", { type: "spawn", configuration:, port: }, &)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def update(configuration, port: :ota, &)
|
|
53
|
+
compile(configuration, &) && upload(configuration, port:, &)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def websocket_command(path, json_message) # rubocop:disable Naming/PredicateMethod
|
|
59
|
+
driver = websocket.get(path).tap(&:raise_for_status).driver
|
|
60
|
+
|
|
61
|
+
driver.on(:open) do
|
|
62
|
+
driver.text(json_message.to_json)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
exit_code = nil
|
|
66
|
+
driver.on(:message) do |msg|
|
|
67
|
+
data = JSON.parse(msg.data)
|
|
68
|
+
case data["event"]
|
|
69
|
+
when "line"
|
|
70
|
+
yield data["data"].gsub("\\033", "\033") if block_given?
|
|
71
|
+
when "exit"
|
|
72
|
+
driver.close
|
|
73
|
+
exit_code = data["code"]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
driver.start
|
|
78
|
+
driver.run
|
|
79
|
+
exit_code.zero?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def websocket
|
|
83
|
+
@websocket ||= @http.plugin(:websocket)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|