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,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
|
data/lib/esphome.rb
ADDED
|
@@ -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:
|
|
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-
|
|
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-
|
|
100
|
+
- esphome-serial-proxy
|
|
101
101
|
extensions: []
|
|
102
102
|
extra_rdoc_files: []
|
|
103
103
|
files:
|
|
104
104
|
- exe/esphome-monitor
|
|
105
|
-
- exe/esphome-
|
|
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.
|
|
169
|
+
version: '3.3'
|
|
120
170
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
171
|
requirements:
|
|
122
172
|
- - ">="
|
data/exe/esphome-update-all
DELETED
|
@@ -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)
|