esphome 1.0.0 → 1.1.1
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 +4 -12
- 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 +373 -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 +54 -2
|
@@ -0,0 +1,373 @@
|
|
|
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
|
+
raise ArgumentError, "missing encryption key" unless encryption_key
|
|
32
|
+
|
|
33
|
+
device = Device.new(address, encryption_key)
|
|
34
|
+
device.connect
|
|
35
|
+
|
|
36
|
+
instance ||= 0
|
|
37
|
+
io = device.serial_proxies[instance]
|
|
38
|
+
begin
|
|
39
|
+
io ||= device.serial_proxies[Integer(instance)]
|
|
40
|
+
rescue ArgumentError
|
|
41
|
+
# ignore; will raise below anyway
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
raise NoSuchSerialProxyError, "No serial proxy #{instance.inspect}" unless io
|
|
45
|
+
|
|
46
|
+
io.set_modem_params(**kwargs) unless kwargs.empty?
|
|
47
|
+
loop_thread = io.instance_variable_set(:@loop_thread, Thread.new do
|
|
48
|
+
Thread.current.report_on_exception = false
|
|
49
|
+
device.loop
|
|
50
|
+
rescue IOError
|
|
51
|
+
raise unless io.closed?
|
|
52
|
+
end)
|
|
53
|
+
io.open
|
|
54
|
+
|
|
55
|
+
if block_given?
|
|
56
|
+
begin
|
|
57
|
+
yield io
|
|
58
|
+
ensure
|
|
59
|
+
io.close
|
|
60
|
+
device.disconnect
|
|
61
|
+
loop_thread.join
|
|
62
|
+
end
|
|
63
|
+
nil
|
|
64
|
+
else
|
|
65
|
+
io
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @!visibility private
|
|
71
|
+
def initialize(device, name, instance, port_type)
|
|
72
|
+
@device = device
|
|
73
|
+
@name = name
|
|
74
|
+
@instance = instance
|
|
75
|
+
@port_type = port_type
|
|
76
|
+
@buffer = "".b
|
|
77
|
+
@mutex = Mutex.new
|
|
78
|
+
@data_ready = ConditionVariable.new
|
|
79
|
+
@request_mutex = Mutex.new
|
|
80
|
+
@closed = true
|
|
81
|
+
@baud = nil
|
|
82
|
+
@data_bits = nil
|
|
83
|
+
@parity = nil
|
|
84
|
+
@stop_bits = nil
|
|
85
|
+
@queue = Queue.new
|
|
86
|
+
|
|
87
|
+
@on_connect_callback = device.on_connect do
|
|
88
|
+
open unless closed?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@on_disconnect_callback = device.on_disconnect do
|
|
92
|
+
@queue << nil
|
|
93
|
+
@mutex.synchronize do
|
|
94
|
+
@data_ready.broadcast
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@on_message_callback = device.on_message do |message|
|
|
99
|
+
case message
|
|
100
|
+
when Api::SerialProxyDataReceived
|
|
101
|
+
next unless message.instance == instance
|
|
102
|
+
|
|
103
|
+
@mutex.synchronize do
|
|
104
|
+
next if @closed
|
|
105
|
+
|
|
106
|
+
@buffer << message.data.b
|
|
107
|
+
@data_ready.broadcast
|
|
108
|
+
end
|
|
109
|
+
when Api::SerialProxyRequestResponse
|
|
110
|
+
next unless message.instance == instance
|
|
111
|
+
|
|
112
|
+
@queue << message
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def set_modem_params(baud: nil, data_bits: nil, parity: nil, stop_bits: nil)
|
|
118
|
+
raise ArgumentError, "Parity must be :none, :even, or :odd" if !parity.nil? && !%i[none even odd].include?(parity)
|
|
119
|
+
raise MissingProxyError unless device
|
|
120
|
+
|
|
121
|
+
current_baud = nil
|
|
122
|
+
current_data_bits = nil
|
|
123
|
+
current_parity = nil
|
|
124
|
+
current_stop_bits = nil
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
@baud = baud || @baud || 115_200
|
|
127
|
+
@data_bits = data_bits || @data_bits || 8
|
|
128
|
+
@parity = parity || @parity || :none
|
|
129
|
+
@stop_bits = stop_bits || @stop_bits || 1
|
|
130
|
+
current_baud = @baud
|
|
131
|
+
current_data_bits = @data_bits
|
|
132
|
+
current_parity = @parity
|
|
133
|
+
current_stop_bits = @stop_bits
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
return unless device.connected?
|
|
137
|
+
|
|
138
|
+
device.send(Api::SerialProxyConfigureRequest.new(instance:,
|
|
139
|
+
baudrate: current_baud,
|
|
140
|
+
flow_control: false,
|
|
141
|
+
parity: normalize_serial_proxy_parity(current_parity),
|
|
142
|
+
stop_bits: current_stop_bits,
|
|
143
|
+
data_size: current_data_bits))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def open
|
|
147
|
+
raise MissingProxyError unless device
|
|
148
|
+
|
|
149
|
+
@request_mutex.synchronize do
|
|
150
|
+
@mutex.synchronize do
|
|
151
|
+
return self unless @closed
|
|
152
|
+
|
|
153
|
+
@closed = false
|
|
154
|
+
@data_ready.broadcast
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
return self unless device.connected?
|
|
158
|
+
|
|
159
|
+
device.send(Api::SerialProxyRequest.new(instance:,
|
|
160
|
+
type: :SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
self
|
|
164
|
+
rescue
|
|
165
|
+
@mutex.synchronize do
|
|
166
|
+
@closed = true
|
|
167
|
+
@data_ready.broadcast
|
|
168
|
+
end
|
|
169
|
+
raise
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def close
|
|
173
|
+
loop_thread = @mutex.synchronize do
|
|
174
|
+
already_closed = @closed
|
|
175
|
+
@closed = true
|
|
176
|
+
@data_ready.broadcast
|
|
177
|
+
[already_closed, @loop_thread]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
return if loop_thread.first
|
|
181
|
+
|
|
182
|
+
# We're in a SerialProxy.open block; just close the connection without unsubscribing
|
|
183
|
+
if loop_thread.last
|
|
184
|
+
device&.disconnect
|
|
185
|
+
loop_thread.last.join
|
|
186
|
+
@mutex.synchronize do
|
|
187
|
+
@loop_thread = nil
|
|
188
|
+
end
|
|
189
|
+
return
|
|
190
|
+
end
|
|
191
|
+
return unless device&.connected?
|
|
192
|
+
|
|
193
|
+
@request_mutex.synchronize do
|
|
194
|
+
return unless device&.connected?
|
|
195
|
+
|
|
196
|
+
device.send(Api::SerialProxyRequest.new(instance:,
|
|
197
|
+
type: :SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE))
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def write(data)
|
|
202
|
+
raise EOFError, "closed stream" if closed?
|
|
203
|
+
raise MissingDeviceError unless device
|
|
204
|
+
raise EOFError, "serial proxy disconnected" unless device.connected?
|
|
205
|
+
|
|
206
|
+
encoded = String(data).b
|
|
207
|
+
device.send(Api::SerialProxyWriteRequest.new(instance:, data: encoded))
|
|
208
|
+
encoded.bytesize
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def flush
|
|
212
|
+
raise EOFError, "closed stream" if closed?
|
|
213
|
+
raise MissingDeviceError unless device
|
|
214
|
+
raise EOFError, "serial proxy disconnected" unless device.connected?
|
|
215
|
+
|
|
216
|
+
@request_mutex.synchronize do
|
|
217
|
+
wait(:SERIAL_PROXY_REQUEST_TYPE_FLUSH) do
|
|
218
|
+
device.send(Api::SerialProxyRequest.new(instance:,
|
|
219
|
+
type: :SERIAL_PROXY_REQUEST_TYPE_FLUSH))
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def read(length, outbuf = +"")
|
|
225
|
+
readpartial(length, outbuf)
|
|
226
|
+
outbuf.concat(readpartial(length - outbuf.bytesize)) while outbuf.bytesize < length
|
|
227
|
+
outbuf
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def readbyte
|
|
231
|
+
read(1)&.getbyte(0)
|
|
232
|
+
end
|
|
233
|
+
alias_method :getbyte, :readbyte
|
|
234
|
+
|
|
235
|
+
def readpartial(maxlen, outbuf = +"")
|
|
236
|
+
raise ArgumentError, "maxlen must be non-negative" unless maxlen >= 0
|
|
237
|
+
|
|
238
|
+
data = wait_for_data do
|
|
239
|
+
raise EOFError if @closed && @buffer.empty?
|
|
240
|
+
break +"".b if maxlen.zero?
|
|
241
|
+
break @buffer.slice!(0, maxlen) unless @buffer.empty?
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
apply_outbuf(outbuf, data)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def read_nonblock(maxlen, outbuf = +"", options = {})
|
|
248
|
+
raise ArgumentError, "maxlen must be non-negative" unless maxlen >= 0
|
|
249
|
+
|
|
250
|
+
if outbuf == ({ exception: false })
|
|
251
|
+
options = outbuf
|
|
252
|
+
outbuf = +""
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
return apply_outbuf(outbuf, +"".b) if maxlen.zero?
|
|
256
|
+
|
|
257
|
+
loop do
|
|
258
|
+
data = @mutex.synchronize do
|
|
259
|
+
raise EOFError if @closed && @buffer.empty?
|
|
260
|
+
|
|
261
|
+
@buffer.slice!(0, maxlen) unless @buffer.empty?
|
|
262
|
+
end
|
|
263
|
+
return apply_outbuf(outbuf, data) if data
|
|
264
|
+
|
|
265
|
+
result = wait_readable(0)
|
|
266
|
+
if result.nil?
|
|
267
|
+
raise WaitReadable unless options[:exception] == false
|
|
268
|
+
|
|
269
|
+
return :wait_readable
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
next
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def wait_readable(timeout = nil)
|
|
277
|
+
return self unless @buffer.empty?
|
|
278
|
+
|
|
279
|
+
@mutex.synchronize do
|
|
280
|
+
return self unless @buffer.empty?
|
|
281
|
+
|
|
282
|
+
deadline = monotonic_time + timeout if timeout
|
|
283
|
+
Kernel.loop do
|
|
284
|
+
return nil if @closed || !device.connected?
|
|
285
|
+
|
|
286
|
+
remaining = deadline && (deadline - monotonic_time)
|
|
287
|
+
return nil if deadline && remaining <= 0
|
|
288
|
+
|
|
289
|
+
@data_ready.wait(@mutex, remaining)
|
|
290
|
+
return self unless @buffer.empty?
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def ready?
|
|
296
|
+
!wait_readable(0).nil?
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def ungetbyte(byte)
|
|
300
|
+
@mutex.synchronize do
|
|
301
|
+
@buffer.insert(0, Integer(byte).chr)
|
|
302
|
+
@data_ready.broadcast
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def ungetc(char)
|
|
307
|
+
@mutex.synchronize do
|
|
308
|
+
@buffer.insert(0, String(char))
|
|
309
|
+
@data_ready.broadcast
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def closed?
|
|
314
|
+
@mutex.synchronize { @closed }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
private
|
|
318
|
+
|
|
319
|
+
def normalize_serial_proxy_parity(parity)
|
|
320
|
+
return parity if parity.to_s.start_with?("SERIAL_PROXY_PARITY_")
|
|
321
|
+
|
|
322
|
+
:"SERIAL_PROXY_PARITY_#{parity.to_s.upcase}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def wait(request_type)
|
|
326
|
+
@queue.clear
|
|
327
|
+
yield
|
|
328
|
+
message = @queue.pop
|
|
329
|
+
raise EOFError unless message
|
|
330
|
+
|
|
331
|
+
unless message.type == request_type
|
|
332
|
+
raise UnexpectedMessage,
|
|
333
|
+
"Unexpected serial proxy response to request type #{message.type}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
case message.status
|
|
337
|
+
when :SERIAL_PROXY_STATUS_OK, :SERIAL_PROXY_STATUS_ASSUMED_SUCCESS
|
|
338
|
+
message.status
|
|
339
|
+
when :SERIAL_PROXY_STATUS_TIMEOUT
|
|
340
|
+
raise Timeout::Error
|
|
341
|
+
when :SERIAL_PROXY_STATUS_NOT_SUPPORTED
|
|
342
|
+
raise NotImplementedError
|
|
343
|
+
when :SERIAL_PROXY_STATUS_ERROR
|
|
344
|
+
raise SerialProxyError, message.error_message
|
|
345
|
+
else
|
|
346
|
+
raise SerialProxyError, "Unrecognized serial proxy status #{message.status.inspect}"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def apply_outbuf(outbuf, data)
|
|
351
|
+
return data unless outbuf
|
|
352
|
+
|
|
353
|
+
outbuf.replace(data)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def wait_for_data
|
|
357
|
+
@mutex.synchronize do
|
|
358
|
+
Kernel.loop do
|
|
359
|
+
raise EOFError if @closed
|
|
360
|
+
|
|
361
|
+
result = yield
|
|
362
|
+
return result unless result.nil?
|
|
363
|
+
|
|
364
|
+
@data_ready.wait(@mutex)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def monotonic_time
|
|
370
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
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: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cody Cutrer
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-07-
|
|
10
|
+
date: 2026-07-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: curses
|
|
@@ -97,10 +97,62 @@ email:
|
|
|
97
97
|
- cody@cutrer.us
|
|
98
98
|
executables:
|
|
99
99
|
- esphome-monitor
|
|
100
|
+
- esphome-serial-proxy
|
|
100
101
|
extensions: []
|
|
101
102
|
extra_rdoc_files: []
|
|
102
103
|
files:
|
|
103
104
|
- exe/esphome-monitor
|
|
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
|
|
104
156
|
homepage: https://github.com/ccutrer/esphome-ruby
|
|
105
157
|
licenses:
|
|
106
158
|
- MIT
|