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,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Lock < Entity
6
+ include HasAssumedState
7
+ include HasState
8
+
9
+ def initialize(_device, list_entities_response)
10
+ super
11
+ @supports_open = list_entities_response.supports_open
12
+ @requires_code = list_entities_response.requires_code
13
+ end
14
+
15
+ def supports_open?
16
+ @supports_open
17
+ end
18
+
19
+ def requires_code?
20
+ @requires_code
21
+ end
22
+
23
+ def update(state_response)
24
+ @state = (state_response.state == :LOCK_STATE_NONE) ? nil : state_response.state[11..].downcase.to_sym
25
+ end
26
+
27
+ def command(command, code = nil)
28
+ message = Api::LockCommandRequest.new(key:, command: :"LOCK_#{command.to_s.upcase}")
29
+ if code
30
+ message.code = code
31
+ message.has_code = true
32
+ end
33
+ device.send(message)
34
+ end
35
+
36
+ def lock(code = nil) = command(:lock, code)
37
+ def unlock(code = nil) = command(:unlock, code)
38
+ def open(code = nil) = command(:open, code)
39
+
40
+ private
41
+
42
+ def inspection_vars
43
+ super + %i[supports_open? requires_code?]
44
+ end
45
+
46
+ def hideable?(var, val)
47
+ super ||
48
+ (var == :supports_open && val) ||
49
+ (var == :requires_code && val)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Number < Entity
6
+ include HasDeviceClass
7
+ include HasState
8
+
9
+ attr_reader :range, :step, :unit_of_measurement, :mode
10
+
11
+ def initialize(_device, list_entities_response)
12
+ super
13
+
14
+ @range = Range.new(list_entities_response.min_value, list_entities_response.max_value)
15
+ @max_value = list_entities_response.max_value
16
+ @step = list_entities_response.step
17
+ @unit_of_measurement = list_entities_response.unit_of_measurement
18
+ @mode = list_entities_response.mode[12..].downcase.to_sym
19
+ end
20
+
21
+ def accuracy_decimals
22
+ str = step.to_s
23
+ return 0 unless str.include?(".")
24
+
25
+ decimals = str.split(".").last
26
+ return 0 if decimals == "0"
27
+
28
+ decimals.size
29
+ end
30
+
31
+ def formatted_state(state = self.state)
32
+ result = if state
33
+ format("%.#{accuracy_decimals}f", state)
34
+ else
35
+ "-"
36
+ end
37
+ result += " #{unit_of_measurement}" if unit_of_measurement
38
+ result
39
+ end
40
+
41
+ def set(state)
42
+ device.send(Api::NumberCommandRequest.new(key:, state:))
43
+ end
44
+
45
+ private
46
+
47
+ def inspection_vars
48
+ super + %i[range step unit_of_measurement mode]
49
+ end
50
+
51
+ def hideable?(var, val)
52
+ super || (var == :mode && val == :auto)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Select < Entity
6
+ include HasState
7
+
8
+ attr_reader :options
9
+
10
+ def initialize(_device, list_entities_response)
11
+ super
12
+
13
+ @options = list_entities_response.options.map(&:freeze).freeze
14
+ end
15
+
16
+ def command(state)
17
+ device.send(Api::SelectCommandRequest.new(key:, state:))
18
+ end
19
+
20
+ private
21
+
22
+ def inspection_vars
23
+ super + [:options]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Sensor < Entity
6
+ include HasDeviceClass
7
+ include HasState
8
+
9
+ attr_reader :unit_of_measurement,
10
+ :accuracy_decimals,
11
+ :state_class
12
+
13
+ def initialize(_device, list_entities_response)
14
+ super
15
+
16
+ @unit_of_measurement = list_entities_response.unit_of_measurement
17
+ @accuracy_decimals = list_entities_response.accuracy_decimals
18
+ @force_update = list_entities_response.force_update
19
+ @state_class = list_entities_response.state_class[12..].downcase.to_sym
20
+ end
21
+
22
+ def force_update?
23
+ @force_update
24
+ end
25
+
26
+ def formatted_state
27
+ result = if state
28
+ format("%.#{accuracy_decimals}f", state)
29
+ else
30
+ "-"
31
+ end
32
+ result += " #{unit_of_measurement}" if unit_of_measurement
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ def inspection_vars
39
+ super + %i[unit_of_measurement accuracy_decimals force_update? state_class]
40
+ end
41
+
42
+ def hideable?(var, val)
43
+ super ||
44
+ (var == :force_update? && !val) ||
45
+ (var == :state_class && val == :none)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Switch < Entity
6
+ include HasDeviceClass
7
+ include HasAssumedState
8
+ include HasState
9
+
10
+ def update(state_response)
11
+ @state = state_response.state
12
+ end
13
+
14
+ def command(state)
15
+ device.send(Api::SwitchCommandRequest.new(key:, state:))
16
+ end
17
+
18
+ def formatted_state = state ? "on" : "off"
19
+
20
+ def on = command(true)
21
+ def off = command(false)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Text < Entity
6
+ include HasState
7
+
8
+ attr_reader :length_range, :pattern, :mode
9
+
10
+ def initialize(_device, list_entities_response)
11
+ super
12
+
13
+ @length_range = Range.new(list_entities_response.min_length, list_entities_response.max_length)
14
+ @pattern = list_entities_response.pattern.empty? ? nil : list_entities_response.pattern
15
+ @mode = list_entities_response.mode[10..].downcase.to_sym
16
+ end
17
+
18
+ def command(state)
19
+ device.send(Api::TextCommandRequest.new(key:, state:))
20
+ end
21
+
22
+ private
23
+
24
+ def inspection_vars
25
+ super + %i[length_range pattern mode]
26
+ end
27
+
28
+ def hideable?(var, val)
29
+ super ||
30
+ (var == :length_range && length_range == (0..0)) ||
31
+ (var == :pattern && pattern.nil?) ||
32
+ (var == :mode && val == :text)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class TextSensor < Entity
6
+ include HasDeviceClass
7
+ include HasState
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Time < Entity
6
+ include HasState
7
+
8
+ def update(state_response)
9
+ @state = if state_response.missing_state
10
+ nil
11
+ elsif defined?(ActiveSupport::Duration)
12
+ state_response.hour.hours +
13
+ state_response.minute.minutes +
14
+ state_response.second.seconds
15
+ else
16
+ [state_response.hour, state_response.minute, state_response.second]
17
+ end
18
+ end
19
+
20
+ def formatted_state
21
+ return format("%02d:%02d:%02d", *state) if state.is_a?(Array)
22
+
23
+ super
24
+ end
25
+
26
+ def command(state)
27
+ if defined?(ActiveSupport::Duration)
28
+ parts = state.parts
29
+ device.send(Api::TimeCommandRequest.new(key:,
30
+ hour: parts[:hours] || 0,
31
+ minute: parts[:minutes] || 0,
32
+ second: parts[:seconds] || 0))
33
+ else
34
+ device.send(Api::TimeCommandRequest.new(key:, hour: state[0], minute: state[1], second: state[2]))
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ class Entity
5
+ module HasState
6
+ def self.included(klass)
7
+ super
8
+
9
+ klass.attr_reader :state
10
+ end
11
+
12
+ # @!visibility private
13
+ def initialize(...)
14
+ super
15
+ @state = nil
16
+ end
17
+
18
+ def update(state_response)
19
+ @state = state_response.missing_state ? nil : state_response.state
20
+ end
21
+
22
+ def formatted_state
23
+ state.nil? ? "-" : state.to_s
24
+ end
25
+
26
+ private
27
+
28
+ def inspection_vars
29
+ super + [:state]
30
+ end
31
+ end
32
+
33
+ module HasAssumedState
34
+ def self.included(klass)
35
+ super
36
+
37
+ klass.attr_reader :assumed_state
38
+ end
39
+
40
+ # @!visibility private
41
+ def initialize(_device, list_entities_response)
42
+ super
43
+
44
+ @assumed_state = list_entities_response.assumed_state
45
+ end
46
+
47
+ def assumed_state?
48
+ @assumed_state
49
+ end
50
+
51
+ private
52
+
53
+ def inspection_vars
54
+ super + [:assumed_state?]
55
+ end
56
+
57
+ def hideable?(var, val)
58
+ super || (var == :assumed_state? && !val)
59
+ end
60
+ end
61
+
62
+ module HasDeviceClass
63
+ def self.included(klass)
64
+ super
65
+
66
+ klass.attr_reader :device_class
67
+ end
68
+
69
+ # @!visibility private
70
+ def initialize(_device, list_entities_response)
71
+ super
72
+
73
+ @device_class = list_entities_response.device_class.empty? ? nil : list_entities_response.device_class
74
+ end
75
+
76
+ private
77
+
78
+ def inspection_vars
79
+ super + [:device_class]
80
+ end
81
+
82
+ def hideable?(var, val)
83
+ super || (var == :device_class && val.nil?)
84
+ end
85
+ end
86
+
87
+ attr_reader :device,
88
+ :object_id_,
89
+ :key,
90
+ :name,
91
+ :icon,
92
+ :disabled_by_default,
93
+ :entity_category
94
+
95
+ # @!visibility private
96
+ def initialize(device, list_entities_response)
97
+ @device = device
98
+ @object_id_ = list_entities_response.name.downcase.tr(" ", "_").gsub(/[^a-z0-9_-]/, "_")
99
+ @key = list_entities_response.key
100
+ @name = list_entities_response.name
101
+ @icon = list_entities_response.icon.empty? ? nil : list_entities_response.icon
102
+ @disabled_by_default = list_entities_response.disabled_by_default
103
+ @entity_category = list_entities_response.entity_category[16..].downcase.to_sym
104
+ end
105
+
106
+ # @return [String]
107
+ def inspect
108
+ vars = inspection_vars.filter_map do |var|
109
+ val = __send__(var)
110
+ next if hideable?(var, val)
111
+
112
+ "#{var}=#{val.inspect}"
113
+ end
114
+ "#<#{self.class.name} #{vars.join(", ")}>"
115
+ end
116
+
117
+ private
118
+
119
+ def inspection_vars
120
+ %i[object_id_ key name icon disabled_by_default entity_category]
121
+ end
122
+
123
+ def hideable?(var, val)
124
+ (var == :disabled_by_default && !val) ||
125
+ (var == :icon && val.nil?) ||
126
+ (var == :entity_category && val == :none)
127
+ end
128
+ end
129
+ end
130
+
131
+ require_relative "entities/binary_sensor"
132
+ require_relative "entities/button"
133
+ require_relative "entities/climate"
134
+ require_relative "entities/cover"
135
+ require_relative "entities/date"
136
+ require_relative "entities/date_time"
137
+ require_relative "entities/fan"
138
+ require_relative "entities/light"
139
+ require_relative "entities/lock"
140
+ require_relative "entities/number"
141
+ require_relative "entities/select"
142
+ require_relative "entities/sensor"
143
+ require_relative "entities/switch"
144
+ require_relative "entities/text"
145
+ require_relative "entities/text_sensor"
146
+ require_relative "entities/time"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ class Error < StandardError; end
5
+
6
+ class DeviceError < Error; end
7
+ class NotConnectedError < Error; end
8
+ class DeviceConnectionError < DeviceError; end
9
+ class AuthenticationError < DeviceError; end
10
+ class InvalidPasswordError < AuthenticationError; end
11
+ class ConnectionClosedError < DeviceConnectionError; end
12
+ class ProtocolError < DeviceError; end
13
+ class PlaintextProtocolError < ProtocolError; end
14
+ class UnknownProtocolError < ProtocolError; end
15
+ class MessageError < DeviceError; end
16
+ class InvalidMessageLengthError < MessageError; end
17
+ class UnknownMessageError < MessageError; end
18
+
19
+ class DashboardError < Error; end
20
+ class NoSuchDeviceError < DashboardError; end
21
+ class MissingEncryptionKeyError < DashboardError; end
22
+
23
+ class SerialProxyError < Error; end
24
+ class NoSuchSerialProxyError < SerialProxyError; end
25
+ class MissingProxyError < SerialProxyError; end
26
+ class MissingDeviceError < SerialProxyError; end
27
+ class UnexpectedMessage < SerialProxyError; end
28
+ end