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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/exe/esphome-monitor +16 -14
  3. data/exe/esphome-serial-proxy +132 -0
  4. data/lib/esphome/action.rb +55 -0
  5. data/lib/esphome/api/README.md +3 -0
  6. data/lib/esphome/api/api.proto +2756 -0
  7. data/lib/esphome/api/api_options.proto +120 -0
  8. data/lib/esphome/api/api_options_pb.rb +20 -0
  9. data/lib/esphome/api/api_pb.rb +229 -0
  10. data/lib/esphome/api.rb +182 -0
  11. data/lib/esphome/cli/colors.rb +44 -0
  12. data/lib/esphome/cli/completion.rb +48 -0
  13. data/lib/esphome/cli/entities/button.rb +18 -0
  14. data/lib/esphome/cli/entities/climate.rb +196 -0
  15. data/lib/esphome/cli/entities/cover.rb +116 -0
  16. data/lib/esphome/cli/entities/date.rb +27 -0
  17. data/lib/esphome/cli/entities/date_time.rb +29 -0
  18. data/lib/esphome/cli/entities/form.rb +111 -0
  19. data/lib/esphome/cli/entities/lock.rb +20 -0
  20. data/lib/esphome/cli/entities/menu.rb +83 -0
  21. data/lib/esphome/cli/entities/number.rb +37 -0
  22. data/lib/esphome/cli/entities/select.rb +16 -0
  23. data/lib/esphome/cli/entities/subfields.rb +63 -0
  24. data/lib/esphome/cli/entities/switch.rb +18 -0
  25. data/lib/esphome/cli/entities/text.rb +16 -0
  26. data/lib/esphome/cli/entities/time.rb +40 -0
  27. data/lib/esphome/cli/entity.rb +78 -0
  28. data/lib/esphome/cli/monitor.rb +441 -0
  29. data/lib/esphome/dashboard.rb +86 -0
  30. data/lib/esphome/device.rb +416 -0
  31. data/lib/esphome/entities/binary_sensor.rb +66 -0
  32. data/lib/esphome/entities/button.rb +13 -0
  33. data/lib/esphome/entities/climate.rb +259 -0
  34. data/lib/esphome/entities/cover.rb +135 -0
  35. data/lib/esphome/entities/date.rb +23 -0
  36. data/lib/esphome/entities/date_time.rb +21 -0
  37. data/lib/esphome/entities/fan.rb +89 -0
  38. data/lib/esphome/entities/light.rb +158 -0
  39. data/lib/esphome/entities/lock.rb +53 -0
  40. data/lib/esphome/entities/number.rb +56 -0
  41. data/lib/esphome/entities/select.rb +27 -0
  42. data/lib/esphome/entities/sensor.rb +49 -0
  43. data/lib/esphome/entities/switch.rb +24 -0
  44. data/lib/esphome/entities/text.rb +36 -0
  45. data/lib/esphome/entities/text_sensor.rb +10 -0
  46. data/lib/esphome/entities/time.rb +39 -0
  47. data/lib/esphome/entity.rb +146 -0
  48. data/lib/esphome/error.rb +28 -0
  49. data/lib/esphome/serial_proxy.rb +372 -0
  50. data/lib/esphome/version.rb +5 -0
  51. data/lib/esphome.rb +5 -0
  52. data/lib/httpx/plugins/websocket.rb +62 -0
  53. data/lib/websocket/driver/httpx.rb +74 -0
  54. metadata +55 -5
  55. data/exe/esphome-update-all +0 -66
@@ -0,0 +1,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file is often required in isolation
4
+ require "esphome"
5
+ require "timeout"
6
+ require "uri"
7
+
8
+ module ESPHome
9
+ class SerialProxy
10
+ class WaitReadable < RuntimeError
11
+ include ::IO::WaitReadable
12
+ end
13
+
14
+ attr_reader :device,
15
+ :name,
16
+ :instance,
17
+ :port_type,
18
+ :baud,
19
+ :data_bits,
20
+ :parity,
21
+ :stop_bits
22
+
23
+ class << self
24
+ def open(address, encryption_key = nil, instance = nil, **kwargs)
25
+ if address.is_a?(URI) && encryption_key.nil? && instance.nil?
26
+ encryption_key = address.password
27
+ instance = address.path[1..]
28
+ instance = nil if instance.empty?
29
+ address = address.host
30
+ end
31
+
32
+ device = Device.new(address, encryption_key)
33
+ device.connect
34
+
35
+ instance ||= 0
36
+ io = device.serial_proxies[instance]
37
+ begin
38
+ io ||= device.serial_proxies[Integer(instance)]
39
+ rescue ArgumentError
40
+ # ignore; will raise below anyway
41
+ end
42
+
43
+ raise NoSuchSerialProxyError, "No serial proxy #{instance.inspect}" unless io
44
+
45
+ io.set_modem_params(**kwargs) unless kwargs.empty?
46
+ loop_thread = io.instance_variable_set(:@loop_thread, Thread.new do
47
+ Thread.current.report_on_exception = false
48
+ device.loop
49
+ rescue IOError
50
+ raise unless io.closed?
51
+ end)
52
+ io.open
53
+
54
+ if block_given?
55
+ begin
56
+ yield io
57
+ ensure
58
+ io.close
59
+ device.disconnect
60
+ loop_thread.join
61
+ end
62
+ nil
63
+ else
64
+ io
65
+ end
66
+ end
67
+ end
68
+
69
+ # @!visibility private
70
+ def initialize(device, name, instance, port_type)
71
+ @device = device
72
+ @name = name
73
+ @instance = instance
74
+ @port_type = port_type
75
+ @buffer = "".b
76
+ @mutex = Mutex.new
77
+ @data_ready = ConditionVariable.new
78
+ @request_mutex = Mutex.new
79
+ @closed = true
80
+ @baud = nil
81
+ @data_bits = nil
82
+ @parity = nil
83
+ @stop_bits = nil
84
+ @queue = Queue.new
85
+
86
+ @on_connect_callback = device.on_connect do
87
+ open unless closed?
88
+ end
89
+
90
+ @on_disconnect_callback = device.on_disconnect do
91
+ @queue << nil
92
+ @mutex.synchronize do
93
+ @data_ready.broadcast
94
+ end
95
+ end
96
+
97
+ @on_message_callback = device.on_message do |message|
98
+ case message
99
+ when Api::SerialProxyDataReceived
100
+ next unless message.instance == instance
101
+
102
+ @mutex.synchronize do
103
+ next if @closed
104
+
105
+ @buffer << message.data.b
106
+ @data_ready.broadcast
107
+ end
108
+ when Api::SerialProxyRequestResponse
109
+ next unless message.instance == instance
110
+
111
+ @queue << message
112
+ end
113
+ end
114
+ end
115
+
116
+ def set_modem_params(baud: nil, data_bits: nil, parity: nil, stop_bits: nil)
117
+ raise ArgumentError, "Parity must be :none, :even, or :odd" if !parity.nil? && !%i[none even odd].include?(parity)
118
+ raise MissingProxyError unless device
119
+
120
+ current_baud = nil
121
+ current_data_bits = nil
122
+ current_parity = nil
123
+ current_stop_bits = nil
124
+ @mutex.synchronize do
125
+ @baud = baud || @baud || 115_200
126
+ @data_bits = data_bits || @data_bits || 8
127
+ @parity = parity || @parity || :none
128
+ @stop_bits = stop_bits || @stop_bits || 1
129
+ current_baud = @baud
130
+ current_data_bits = @data_bits
131
+ current_parity = @parity
132
+ current_stop_bits = @stop_bits
133
+ end
134
+
135
+ return unless device.connected?
136
+
137
+ device.send(Api::SerialProxyConfigureRequest.new(instance:,
138
+ baudrate: current_baud,
139
+ flow_control: false,
140
+ parity: normalize_serial_proxy_parity(current_parity),
141
+ stop_bits: current_stop_bits,
142
+ data_size: current_data_bits))
143
+ end
144
+
145
+ def open
146
+ raise MissingProxyError unless device
147
+
148
+ @request_mutex.synchronize do
149
+ @mutex.synchronize do
150
+ return self unless @closed
151
+
152
+ @closed = false
153
+ @data_ready.broadcast
154
+ end
155
+
156
+ return self unless device.connected?
157
+
158
+ device.send(Api::SerialProxyRequest.new(instance:,
159
+ type: :SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE))
160
+ end
161
+
162
+ self
163
+ rescue
164
+ @mutex.synchronize do
165
+ @closed = true
166
+ @data_ready.broadcast
167
+ end
168
+ raise
169
+ end
170
+
171
+ def close
172
+ loop_thread = @mutex.synchronize do
173
+ already_closed = @closed
174
+ @closed = true
175
+ @data_ready.broadcast
176
+ [already_closed, @loop_thread]
177
+ end
178
+
179
+ return if loop_thread.first
180
+
181
+ # We're in a SerialProxy.open block; just close the connection without unsubscribing
182
+ if loop_thread.last
183
+ device&.disconnect
184
+ loop_thread.last.join
185
+ @mutex.synchronize do
186
+ @loop_thread = nil
187
+ end
188
+ return
189
+ end
190
+ return unless device&.connected?
191
+
192
+ @request_mutex.synchronize do
193
+ return unless device&.connected?
194
+
195
+ device.send(Api::SerialProxyRequest.new(instance:,
196
+ type: :SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE))
197
+ end
198
+ end
199
+
200
+ def write(data)
201
+ raise EOFError, "closed stream" if closed?
202
+ raise MissingDeviceError unless device
203
+ raise EOFError, "serial proxy disconnected" unless device.connected?
204
+
205
+ encoded = String(data).b
206
+ device.send(Api::SerialProxyWriteRequest.new(instance:, data: encoded))
207
+ encoded.bytesize
208
+ end
209
+
210
+ def flush
211
+ raise EOFError, "closed stream" if closed?
212
+ raise MissingDeviceError unless device
213
+ raise EOFError, "serial proxy disconnected" unless device.connected?
214
+
215
+ @request_mutex.synchronize do
216
+ wait(:SERIAL_PROXY_REQUEST_TYPE_FLUSH) do
217
+ device.send(Api::SerialProxyRequest.new(instance:,
218
+ type: :SERIAL_PROXY_REQUEST_TYPE_FLUSH))
219
+ end
220
+ end
221
+ end
222
+
223
+ def read(length, outbuf = +"")
224
+ readpartial(length, outbuf)
225
+ outbuf.concat(readpartial(length - outbuf.bytesize)) while outbuf.bytesize < length
226
+ outbuf
227
+ end
228
+
229
+ def readbyte
230
+ read(1)&.getbyte(0)
231
+ end
232
+ alias_method :getbyte, :readbyte
233
+
234
+ def readpartial(maxlen, outbuf = +"")
235
+ raise ArgumentError, "maxlen must be non-negative" unless maxlen >= 0
236
+
237
+ data = wait_for_data do
238
+ raise EOFError if @closed && @buffer.empty?
239
+ break +"".b if maxlen.zero?
240
+ break @buffer.slice!(0, maxlen) unless @buffer.empty?
241
+ end
242
+
243
+ apply_outbuf(outbuf, data)
244
+ end
245
+
246
+ def read_nonblock(maxlen, outbuf = +"", options = {})
247
+ raise ArgumentError, "maxlen must be non-negative" unless maxlen >= 0
248
+
249
+ if outbuf == ({ exception: false })
250
+ options = outbuf
251
+ outbuf = +""
252
+ end
253
+
254
+ return apply_outbuf(outbuf, +"".b) if maxlen.zero?
255
+
256
+ loop do
257
+ data = @mutex.synchronize do
258
+ raise EOFError if @closed && @buffer.empty?
259
+
260
+ @buffer.slice!(0, maxlen) unless @buffer.empty?
261
+ end
262
+ return apply_outbuf(outbuf, data) if data
263
+
264
+ result = wait_readable(0)
265
+ if result.nil?
266
+ raise WaitReadable unless options[:exception] == false
267
+
268
+ return :wait_readable
269
+ end
270
+
271
+ next
272
+ end
273
+ end
274
+
275
+ def wait_readable(timeout = nil)
276
+ return self unless @buffer.empty?
277
+
278
+ @mutex.synchronize do
279
+ return self unless @buffer.empty?
280
+
281
+ deadline = monotonic_time + timeout if timeout
282
+ Kernel.loop do
283
+ return nil if @closed || !device.connected?
284
+
285
+ remaining = deadline && (deadline - monotonic_time)
286
+ return nil if deadline && remaining <= 0
287
+
288
+ @data_ready.wait(@mutex, remaining)
289
+ return self unless @buffer.empty?
290
+ end
291
+ end
292
+ end
293
+
294
+ def ready?
295
+ !wait_readable(0).nil?
296
+ end
297
+
298
+ def ungetbyte(byte)
299
+ @mutex.synchronize do
300
+ @buffer.insert(0, Integer(byte).chr)
301
+ @data_ready.broadcast
302
+ end
303
+ end
304
+
305
+ def ungetc(char)
306
+ @mutex.synchronize do
307
+ @buffer.insert(0, String(char))
308
+ @data_ready.broadcast
309
+ end
310
+ end
311
+
312
+ def closed?
313
+ @mutex.synchronize { @closed }
314
+ end
315
+
316
+ private
317
+
318
+ def normalize_serial_proxy_parity(parity)
319
+ return parity if parity.to_s.start_with?("SERIAL_PROXY_PARITY_")
320
+
321
+ :"SERIAL_PROXY_PARITY_#{parity.to_s.upcase}"
322
+ end
323
+
324
+ def wait(request_type)
325
+ @queue.clear
326
+ yield
327
+ message = @queue.pop
328
+ raise EOFError unless message
329
+
330
+ unless message.type == request_type
331
+ raise UnexpectedMessage,
332
+ "Unexpected serial proxy response to request type #{message.type}"
333
+ end
334
+
335
+ case message.status
336
+ when :SERIAL_PROXY_STATUS_OK, :SERIAL_PROXY_STATUS_ASSUMED_SUCCESS
337
+ message.status
338
+ when :SERIAL_PROXY_STATUS_TIMEOUT
339
+ raise Timeout::Error
340
+ when :SERIAL_PROXY_STATUS_NOT_SUPPORTED
341
+ raise NotImplementedError
342
+ when :SERIAL_PROXY_STATUS_ERROR
343
+ raise SerialProxyError, message.error_message
344
+ else
345
+ raise SerialProxyError, "Unrecognized serial proxy status #{message.status.inspect}"
346
+ end
347
+ end
348
+
349
+ def apply_outbuf(outbuf, data)
350
+ return data unless outbuf
351
+
352
+ outbuf.replace(data)
353
+ end
354
+
355
+ def wait_for_data
356
+ @mutex.synchronize do
357
+ Kernel.loop do
358
+ raise EOFError if @closed
359
+
360
+ result = yield
361
+ return result unless result.nil?
362
+
363
+ @data_ready.wait(@mutex)
364
+ end
365
+ end
366
+ end
367
+
368
+ def monotonic_time
369
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ VERSION = "1.1.0"
5
+ end
data/lib/esphome.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "esphome/error"
4
+ require_relative "esphome/device"
5
+ require_relative "esphome/version"
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "websocket/driver"
5
+ require "websocket/driver/httpx"
6
+
7
+ module HTTPX
8
+ module Plugins
9
+ module Websocket
10
+ class << self
11
+ def load_dependencies(klass)
12
+ klass.plugin(:upgrade)
13
+ end
14
+
15
+ def call(connection, request, response)
16
+ return unless (driver = request.driver)
17
+
18
+ return unless driver.send(:validate_handshake, response.headers)
19
+
20
+ connection.hijack_io
21
+ response.instance_variable_set(:@driver, driver)
22
+ driver.instance_variable_set(:@initial_response, response.body.to_s)
23
+ end
24
+
25
+ def extra_options(options)
26
+ options.merge(max_concurrent_requests: 1,
27
+ upgrade_handlers: options.upgrade_handlers.merge("websocket" => self))
28
+ end
29
+ end
30
+
31
+ module ConnectionMethods
32
+ def send(request)
33
+ request.init_websocket(self) unless request.driver || @upgrade_protocol
34
+
35
+ super
36
+ end
37
+ end
38
+
39
+ module RequestMethods
40
+ attr_reader :driver
41
+
42
+ def init_websocket(connection)
43
+ if connection.state == :open
44
+ socket = connection.to_io
45
+ @driver = WebSocket::Driver::HTTPX.new(socket, @headers, { masking: true })
46
+ else
47
+ connection.once(:open) do
48
+ socket = connection.to_io
49
+ @driver = WebSocket::Driver::HTTPX.new(socket, @headers, { masking: true })
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ module ResponseMethods
56
+ attr_reader :driver
57
+ end
58
+ end
59
+
60
+ register_plugin(:websocket, Websocket)
61
+ end
62
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebSocket
4
+ class Driver
5
+ # WebSocket driver with HTTPX handling the HTTP negotiation
6
+ class HTTPX < Hybi
7
+ def initialize(socket, headers, options = {})
8
+ super(socket, options)
9
+ @headers = headers
10
+ @closed = false
11
+
12
+ @key = Client.generate_key
13
+ @accept = Driver::Hybi.generate_accept(@key)
14
+
15
+ @headers["upgrade"] = "websocket"
16
+ @headers["connection"] = "Upgrade"
17
+ @headers["sec-websocket-key"] = @key
18
+ @headers["sec-websocket-version"] = VERSION
19
+
20
+ @headers["Sec-WebSocket-Protocol"] = @protocols * ", " if @protocols.size.positive?
21
+
22
+ extensions = @extensions.generate_offer
23
+ @headers["Sec-WebSocket-Extensions"] = extensions if extensions
24
+ end
25
+
26
+ def start
27
+ open
28
+ parse(@initial_response)
29
+ end
30
+
31
+ def run
32
+ closed = false
33
+ on(:close) do
34
+ closed = true
35
+ end
36
+
37
+ until closed
38
+ bytes = @socket.read(1)
39
+ parse(bytes)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def fail_handshake(message) # rubocop:disable Naming/PredicateMethod
46
+ message = "Error during WebSocket handshake: #{message}"
47
+ @ready_state = 3
48
+ emit(:error, message)
49
+ emit(:close, Driver::CloseEvent.new(Driver::Hybi::ERRORS[:protocol_error], message))
50
+ false
51
+ end
52
+
53
+ def validate_handshake(headers)
54
+ accept = headers["sec-websocket-accept"]
55
+ protocol = headers["sec-websocket-protocol"]
56
+
57
+ return fail_handshake("Sec-WebSocket-Accept mismatch") unless accept == @accept
58
+
59
+ if protocol && !protocol.empty?
60
+ return fail_handshake("Sec-WebSocket-Protocol mismatch") unless @protocols.include?(protocol)
61
+
62
+ @protocol = protocol
63
+ end
64
+
65
+ begin
66
+ @extensions.activate(@headers["Sec-WebSocket-Extensions"])
67
+ rescue ::WebSocket::Extensions::ExtensionError => e
68
+ return fail_handshake(e.message)
69
+ end
70
+ true
71
+ end
72
+ end
73
+ end
74
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: esphome
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-07 00:00:00.000000000 Z
10
+ date: 2026-07-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: curses
@@ -97,12 +97,62 @@ email:
97
97
  - cody@cutrer.us
98
98
  executables:
99
99
  - esphome-monitor
100
- - esphome-update-all
100
+ - esphome-serial-proxy
101
101
  extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
104
  - exe/esphome-monitor
105
- - exe/esphome-update-all
105
+ - exe/esphome-serial-proxy
106
+ - lib/esphome.rb
107
+ - lib/esphome/action.rb
108
+ - lib/esphome/api.rb
109
+ - lib/esphome/api/README.md
110
+ - lib/esphome/api/api.proto
111
+ - lib/esphome/api/api_options.proto
112
+ - lib/esphome/api/api_options_pb.rb
113
+ - lib/esphome/api/api_pb.rb
114
+ - lib/esphome/cli/colors.rb
115
+ - lib/esphome/cli/completion.rb
116
+ - lib/esphome/cli/entities/button.rb
117
+ - lib/esphome/cli/entities/climate.rb
118
+ - lib/esphome/cli/entities/cover.rb
119
+ - lib/esphome/cli/entities/date.rb
120
+ - lib/esphome/cli/entities/date_time.rb
121
+ - lib/esphome/cli/entities/form.rb
122
+ - lib/esphome/cli/entities/lock.rb
123
+ - lib/esphome/cli/entities/menu.rb
124
+ - lib/esphome/cli/entities/number.rb
125
+ - lib/esphome/cli/entities/select.rb
126
+ - lib/esphome/cli/entities/subfields.rb
127
+ - lib/esphome/cli/entities/switch.rb
128
+ - lib/esphome/cli/entities/text.rb
129
+ - lib/esphome/cli/entities/time.rb
130
+ - lib/esphome/cli/entity.rb
131
+ - lib/esphome/cli/monitor.rb
132
+ - lib/esphome/dashboard.rb
133
+ - lib/esphome/device.rb
134
+ - lib/esphome/entities/binary_sensor.rb
135
+ - lib/esphome/entities/button.rb
136
+ - lib/esphome/entities/climate.rb
137
+ - lib/esphome/entities/cover.rb
138
+ - lib/esphome/entities/date.rb
139
+ - lib/esphome/entities/date_time.rb
140
+ - lib/esphome/entities/fan.rb
141
+ - lib/esphome/entities/light.rb
142
+ - lib/esphome/entities/lock.rb
143
+ - lib/esphome/entities/number.rb
144
+ - lib/esphome/entities/select.rb
145
+ - lib/esphome/entities/sensor.rb
146
+ - lib/esphome/entities/switch.rb
147
+ - lib/esphome/entities/text.rb
148
+ - lib/esphome/entities/text_sensor.rb
149
+ - lib/esphome/entities/time.rb
150
+ - lib/esphome/entity.rb
151
+ - lib/esphome/error.rb
152
+ - lib/esphome/serial_proxy.rb
153
+ - lib/esphome/version.rb
154
+ - lib/httpx/plugins/websocket.rb
155
+ - lib/websocket/driver/httpx.rb
106
156
  homepage: https://github.com/ccutrer/esphome-ruby
107
157
  licenses:
108
158
  - MIT
@@ -116,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
166
  requirements:
117
167
  - - ">="
118
168
  - !ruby/object:Gem::Version
119
- version: 3.1.0
169
+ version: '3.3'
120
170
  required_rubygems_version: !ruby/object:Gem::Requirement
121
171
  requirements:
122
172
  - - ">="
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "optparse"
5
-
6
- uri = "http://localhost:6052/"
7
- opts = OptionParser.new do |opts|
8
- opts.banner = <<~TEXT
9
- Usage: esphome-update-all [<uri>] <device_name...>
10
-
11
- Example: esphome-update-all
12
- If URI is omitted, it defaults to http://localhost:6052/
13
- TEXT
14
-
15
- opts.on("-h", "--help", "Show this help message") do
16
- puts opts
17
- exit
18
- end
19
- end
20
- opts.parse!
21
-
22
- uri = ARGV.shift if ARGV.first&.include?(":")
23
-
24
- require "esphome/dashboard"
25
-
26
- dashboard = ESPHome::Dashboard.new(uri)
27
-
28
- devices = dashboard.devices
29
-
30
- unless ARGV.empty?
31
- all_devices = devices
32
- devices = []
33
- ARGV.each do |device_name|
34
- devices.concat(all_devices.select { |d| File.fnmatch(device_name, d["name"]) })
35
- end
36
- devices.uniq!
37
-
38
- puts "Updating #{devices.map { |d| d["name"] }.join(", ")}..."
39
- end
40
-
41
- if devices.empty?
42
- warn "No matching devices found."
43
- exit 1
44
- end
45
-
46
- results = dashboard.update_all(devices) do |line|
47
- $stdout.print line
48
- end
49
-
50
- puts "\n\n"
51
- successes, failures = results.partition { |_, v| v }
52
-
53
- {
54
- Successful: successes,
55
- Failed: failures
56
- }.each do |status, group|
57
- next if group.empty?
58
-
59
- puts "#{status} Updates:\n"
60
-
61
- group.each do |device, _| # rubocop:disable Style/HashEachMethods
62
- puts " - #{device["name"]}"
63
- end
64
- end
65
-
66
- exit(failures.empty? ? 0 : 1)