Class: ESPHome::Device

Inherits:
Object
  • Object
show all
Defined in:
lib/esphome/device.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(address, encryption_key, port: 6053) ⇒ Device

Returns a new instance of Device.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/esphome/device.rb', line 56

def initialize(address, encryption_key, port: 6053)
  @address = address
  @encryption_key = encryption_key.unpack1("m0")
  @port = port
  @socket = nil
  @connection_logger = nil
  @device_logger = nil
  @noise = nil
  @on_connect_callback = []
  @on_disconnect_callback = []
  @on_message_callback = []
  @entities = nil
  @connect_timeout = 5
  @read_timeout = 5
  @messages_to_replay = []
  @serial_proxies = {}.freeze
end

Instance Attribute Details

#addressObject (readonly)

Returns the value of attribute address.



41
42
43
# File 'lib/esphome/device.rb', line 41

def address
  @address
end

#compilation_timeObject (readonly)

Returns the value of attribute compilation_time.



41
42
43
# File 'lib/esphome/device.rb', line 41

def compilation_time
  @compilation_time
end

#connect_timeoutObject

Returns the value of attribute connect_timeout.



54
55
56
# File 'lib/esphome/device.rb', line 54

def connect_timeout
  @connect_timeout
end

#connection_loggerObject

Returns the value of attribute connection_logger.



54
55
56
# File 'lib/esphome/device.rb', line 54

def connection_logger
  @connection_logger
end

#device_loggerObject

Returns the value of attribute device_logger.



54
55
56
# File 'lib/esphome/device.rb', line 54

def device_logger
  @device_logger
end

#esphome_versionObject (readonly)

Returns the value of attribute esphome_version.



41
42
43
# File 'lib/esphome/device.rb', line 41

def esphome_version
  @esphome_version
end

#friendly_nameObject (readonly)

Returns the value of attribute friendly_name.



41
42
43
# File 'lib/esphome/device.rb', line 41

def friendly_name
  @friendly_name
end

#mac_addressObject (readonly)

Returns the value of attribute mac_address.



41
42
43
# File 'lib/esphome/device.rb', line 41

def mac_address
  @mac_address
end

#manufacturerObject (readonly)

Returns the value of attribute manufacturer.



41
42
43
# File 'lib/esphome/device.rb', line 41

def manufacturer
  @manufacturer
end

#modelObject (readonly)

Returns the value of attribute model.



41
42
43
# File 'lib/esphome/device.rb', line 41

def model
  @model
end

#nameObject (readonly)

Returns the value of attribute name.



41
42
43
# File 'lib/esphome/device.rb', line 41

def name
  @name
end

#portObject (readonly)

Returns the value of attribute port.



41
42
43
# File 'lib/esphome/device.rb', line 41

def port
  @port
end

#project_nameObject (readonly)

Returns the value of attribute project_name.



41
42
43
# File 'lib/esphome/device.rb', line 41

def project_name
  @project_name
end

#project_versionObject (readonly)

Returns the value of attribute project_version.



41
42
43
# File 'lib/esphome/device.rb', line 41

def project_version
  @project_version
end

#read_timeoutObject

Returns the value of attribute read_timeout.



54
55
56
# File 'lib/esphome/device.rb', line 54

def read_timeout
  @read_timeout
end

#serial_proxiesObject (readonly)

Returns the value of attribute serial_proxies.



41
42
43
# File 'lib/esphome/device.rb', line 41

def serial_proxies
  @serial_proxies
end

#suggested_areaObject (readonly)

Returns the value of attribute suggested_area.



41
42
43
# File 'lib/esphome/device.rb', line 41

def suggested_area
  @suggested_area
end

Instance Method Details

#connectObject



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/esphome/device.rb', line 74

def connect
  return if @socket

  addresses = resolve
  exception = nil
  addresses.each do |addrinfo|
    @socket = addrinfo.connect
  rescue => e
    exception = e
  end
  if exception
    raise DeviceConnectionError,
          "Unable to connect to #{Array(address).join(", ")}:#{port}: #{exception.message}"
  end

  begin
    # Noise logs warnings about not being able to load algorithms we don't even care about.
    old_level = Noise.logger.level
    Noise.logger.level = Logger::ERROR
    noise = Noise::Connection::Initiator.new(NOISE_PROTOCOL)
  ensure
    Noise.logger.level = old_level
  end
  noise.psks = [@encryption_key]
  noise.prologue = NOISE_PROLOGUE
  noise.start_handshake

  write_frame("")
  write_frame("\0#{noise.write_message}")

  _device_id = read_frame
  noise.read_message(read_frame[1..])
  @noise = noise

  send(Api::HelloRequest.new(client_info: "esphome-ruby",
                             api_version_major: API_VERSION_MAJOR,
                             api_version_minor: API_VERSION_MINOR))

  read_messages do |message|
    if message.is_a?(Api::AuthenticationResponse)
      raise InvalidPasswordError, "Invalid password" if message.invalid_password

      next true
    end

    next false unless message.is_a?(Api::HelloResponse)

    break
  end

  send(Api::DeviceInfoRequest.new)

  read_messages do |message|
    if message.is_a?(Api::AuthenticationResponse)
      raise InvalidPasswordError, "Invalid password" if message.invalid_password

      next true
    end

    next false unless message.is_a?(Api::DeviceInfoResponse)

    @name = message.name
    @mac_address = message.mac_address
    @esphome_version = message.esphome_version
    @compilation_time = message.compilation_time
    @model = message.model
    @project_name = message.project_name
    @project_version = message.project_version
    @manufacturer = message.manufacturer
    @friendly_name = message.friendly_name
    @suggested_area = message.suggested_area
    old_proxies = @serial_proxies
    @serial_proxies = {}

    message.serial_proxies.each_with_index do |proxy, instance|
      name = proxy.name.freeze

      # keep the same object post-reconnect
      proxy = old_proxies[name] if old_proxies[name]&.instance == instance
      proxy = SerialProxy.new(self, name, instance, normalize_serial_proxy_port_type(proxy.port_type))

      @serial_proxies[name] = proxy
      @serial_proxies[proxy.instance] = proxy
    end
    @serial_proxies.freeze

    missing_proxies = old_proxies.values.uniq - @serial_proxies.values
    missing_proxies.each do |proxy|
      proxy.instance_variable_set(:@closed, true)
      proxy.instance_variable_set(:@device, nil)
    end

    @entities = nil

    @on_connect_callback.each(&:call)
    break
  end
end

#connected?Boolean

Returns:

  • (Boolean)


183
184
185
# File 'lib/esphome/device.rb', line 183

def connected?
  !!(@socket && @noise)
end

#disconnectObject



173
174
175
176
177
178
179
180
181
# File 'lib/esphome/device.rb', line 173

def disconnect
  return unless connected?

  send(Api::DisconnectRequest.new)
rescue DeviceConnectionError, NotConnectedError, IOError, SocketError, SystemCallError
  nil
ensure
  disconnected
end

#entitiesObject



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/esphome/device.rb', line 187

def entities
  return @entities if @entities

  send(Api::ListEntitiesRequest.new)

  @entities = {}
  read_messages do |message|
    break if message.is_a?(Api::ListEntitiesDoneResponse)

    next false unless message.class.respond_to?(:entity_class)

    entity_class = message.class.entity_class
    @entities[message.key] = entity_class.new(self, message)
    true
  end
  @entities.freeze
end

#loopObject



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/esphome/device.rb', line 219

def loop
  Kernel.loop do
    message = nil
    begin
      message = if @messages_to_replay.empty?
                  read_message
                else
                  @messages_to_replay.shift
                end
    rescue Timeout::Error
      send(Api::PingRequest.new) if connected?
      next
    end

    if message.is_a?(Api::PingRequest)
      send(Api::PingResponse.new)
      next
    end

    if message.respond_to?(:key) && (entity = @entities[message.key])
      entity.update(message)
      @on_message_callback.each { |callback| callback.call(entity) }
    elsif message.is_a?(Api::SubscribeLogsResponse)
      device_logger&.log(LOG_LEVEL_MAP[message.level], message.message)
    elsif message.is_a?(Api::HomeassistantActionRequest)
      action = Action.from_protobuf(message)
      @on_message_callback.each { |callback| callback.call(action) }
    elsif message.is_a?(Api::DisconnectRequest)
      send(Api::DisconnectResponse.new)
      disconnected
      @on_disconnect_callback.each(&:call)
    elsif message.is_a?(Api::DisconnectResponse)
      disconnected
    elsif message.is_a?(Api::PingResponse)
      # Nothing to do
    else
      @on_message_callback.each { |callback| callback.call(message) }
    end
  end
rescue Interrupt
  nil
end

#send(message) ⇒ Object

Raises:



276
277
278
279
280
281
282
283
284
285
286
# File 'lib/esphome/device.rb', line 276

def send(message)
  raise NotConnectedError, "Encryption not yet set up" unless connected?

  connection_logger&.debug { "> #{message.inspect}" }
  serialized_message = message.to_proto
  unencrypted_message = [message.class.descriptor.id,
                         serialized_message.length,
                         serialized_message].pack("nnA*")
  encrypted_message = @noise.encrypt(unencrypted_message)
  write_frame(encrypted_message)
end

#stream_actionsObject



215
216
217
# File 'lib/esphome/device.rb', line 215

def stream_actions
  send(Api::SubscribeHomeassistantServicesRequest.new)
end

#stream_log(level = :very_verbose, dump_config: false) ⇒ Object



209
210
211
212
213
# File 'lib/esphome/device.rb', line 209

def stream_log(level = :very_verbose, dump_config: false)
  request = Api::SubscribeLogsRequest.new(level: :"LOG_LEVEL_#{level.upcase}")
  request.dump_config = dump_config if dump_config
  send(request)
end

#stream_statesObject



205
206
207
# File 'lib/esphome/device.rb', line 205

def stream_states
  send(Api::SubscribeStatesRequest.new)
end