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,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "api/api_pb"
4
+ require_relative "action"
5
+ require_relative "entity"
6
+
7
+ module ESPHome
8
+ module Api
9
+ ID_TO_MESSAGE = [nil,
10
+ HelloRequest,
11
+ HelloResponse,
12
+ AuthenticationRequest,
13
+ AuthenticationResponse,
14
+ DisconnectRequest,
15
+ DisconnectResponse,
16
+ PingRequest,
17
+ PingResponse,
18
+ DeviceInfoRequest,
19
+ DeviceInfoResponse,
20
+ ListEntitiesRequest,
21
+ ListEntitiesBinarySensorResponse,
22
+ ListEntitiesCoverResponse,
23
+ ListEntitiesFanResponse,
24
+ ListEntitiesLightResponse,
25
+ ListEntitiesSensorResponse,
26
+ ListEntitiesSwitchResponse,
27
+ ListEntitiesTextSensorResponse,
28
+ ListEntitiesDoneResponse,
29
+ SubscribeStatesRequest,
30
+ BinarySensorStateResponse,
31
+ CoverStateResponse,
32
+ FanStateResponse,
33
+ LightStateResponse,
34
+ SensorStateResponse,
35
+ SwitchStateResponse,
36
+ TextSensorStateResponse,
37
+ SubscribeLogsRequest,
38
+ SubscribeLogsResponse,
39
+ CoverCommandRequest,
40
+ FanCommandRequest,
41
+ LightCommandRequest,
42
+ SwitchCommandRequest,
43
+ SubscribeHomeassistantServicesRequest,
44
+ HomeassistantActionRequest,
45
+ GetTimeRequest,
46
+ GetTimeResponse,
47
+ SubscribeHomeAssistantStatesRequest,
48
+ SubscribeHomeAssistantStateResponse,
49
+ HomeAssistantStateResponse,
50
+ ListEntitiesServicesResponse,
51
+ ExecuteServiceRequest,
52
+ ListEntitiesCameraResponse,
53
+ CameraImageResponse,
54
+ CameraImageRequest,
55
+ ListEntitiesClimateResponse,
56
+ ClimateStateResponse,
57
+ ClimateCommandRequest,
58
+ ListEntitiesNumberResponse,
59
+ NumberStateResponse,
60
+ NumberCommandRequest,
61
+ ListEntitiesSelectResponse,
62
+ SelectStateResponse,
63
+ SelectCommandRequest,
64
+ ListEntitiesSirenResponse,
65
+ SirenStateResponse,
66
+ SirenCommandRequest,
67
+ ListEntitiesLockResponse,
68
+ LockStateResponse,
69
+ LockCommandRequest,
70
+ ListEntitiesButtonResponse,
71
+ ButtonCommandRequest,
72
+ ListEntitiesMediaPlayerResponse,
73
+ MediaPlayerStateResponse,
74
+ MediaPlayerCommandRequest,
75
+ SubscribeBluetoothLEAdvertisementsRequest,
76
+ BluetoothLEAdvertisementResponse,
77
+ BluetoothDeviceRequest,
78
+ BluetoothDeviceConnectionResponse,
79
+ BluetoothGATTGetServicesRequest,
80
+ BluetoothGATTGetServicesResponse,
81
+ BluetoothGATTGetServicesDoneResponse,
82
+ BluetoothGATTReadRequest,
83
+ BluetoothGATTReadResponse,
84
+ BluetoothGATTWriteRequest,
85
+ BluetoothGATTReadDescriptorRequest,
86
+ BluetoothGATTWriteDescriptorRequest,
87
+ BluetoothGATTNotifyRequest,
88
+ BluetoothGATTNotifyDataResponse,
89
+ SubscribeBluetoothConnectionsFreeRequest,
90
+ BluetoothConnectionsFreeResponse,
91
+ BluetoothGATTErrorResponse,
92
+ BluetoothGATTWriteResponse,
93
+ BluetoothGATTNotifyResponse,
94
+ BluetoothDevicePairingResponse,
95
+ BluetoothDeviceUnpairingResponse,
96
+ UnsubscribeBluetoothLEAdvertisementsRequest,
97
+ BluetoothDeviceClearCacheResponse,
98
+ SubscribeVoiceAssistantRequest,
99
+ VoiceAssistantRequest,
100
+ VoiceAssistantResponse,
101
+ VoiceAssistantEventResponse,
102
+ BluetoothLERawAdvertisementsResponse,
103
+ ListEntitiesAlarmControlPanelResponse,
104
+ AlarmControlPanelStateResponse,
105
+ AlarmControlPanelCommandRequest,
106
+ ListEntitiesTextResponse,
107
+ TextStateResponse,
108
+ TextCommandRequest,
109
+ ListEntitiesDateResponse,
110
+ DateStateResponse,
111
+ DateCommandRequest,
112
+ ListEntitiesTimeResponse,
113
+ TimeStateResponse,
114
+ TimeCommandRequest,
115
+ VoiceAssistantAudio,
116
+ ListEntitiesEventResponse,
117
+ EventResponse,
118
+ ListEntitiesValveResponse,
119
+ ValveStateResponse,
120
+ ValveCommandRequest,
121
+ ListEntitiesDateTimeResponse,
122
+ DateTimeStateResponse,
123
+ DateTimeCommandRequest,
124
+ VoiceAssistantTimerEventResponse,
125
+ ListEntitiesUpdateResponse,
126
+ UpdateStateResponse,
127
+ UpdateCommandRequest,
128
+ VoiceAssistantAnnounceRequest,
129
+ VoiceAssistantAnnounceFinished,
130
+ VoiceAssistantConfigurationRequest,
131
+ VoiceAssistantConfigurationResponse,
132
+ VoiceAssistantSetConfiguration,
133
+ NoiseEncryptionSetKeyRequest,
134
+ NoiseEncryptionSetKeyResponse,
135
+ BluetoothScannerStateResponse,
136
+ BluetoothScannerSetModeRequest,
137
+ ZWaveProxyFrame,
138
+ ZWaveProxyRequest,
139
+ HomeassistantActionResponse,
140
+ ExecuteServiceResponse,
141
+ ListEntitiesWaterHeaterResponse,
142
+ WaterHeaterStateResponse,
143
+ WaterHeaterCommandRequest,
144
+ ListEntitiesInfraredResponse,
145
+ InfraredRFTransmitRawTimingsRequest,
146
+ InfraredRFReceiveEvent,
147
+ SerialProxyConfigureRequest,
148
+ SerialProxyDataReceived,
149
+ SerialProxyWriteRequest,
150
+ SerialProxySetModemPinsRequest,
151
+ SerialProxyGetModemPinsRequest,
152
+ SerialProxyGetModemPinsResponse,
153
+ SerialProxyRequest,
154
+ BluetoothSetConnectionParamsRequest,
155
+ BluetoothSetConnectionParamsResponse,
156
+ SerialProxyRequestResponse,
157
+ ListEntitiesRadioFrequencyResponse].each_with_index.filter_map do |klass, index|
158
+ next unless klass
159
+
160
+ klass.descriptor.define_singleton_method(:id) { index }
161
+
162
+ [index, klass]
163
+ end.to_h.freeze
164
+
165
+ def ListEntitiesBinarySensorResponse.entity_class = Entities::BinarySensor
166
+ def ListEntitiesButtonResponse.entity_class = Entities::Button
167
+ def ListEntitiesClimateResponse.entity_class = Entities::Climate
168
+ def ListEntitiesCoverResponse.entity_class = Entities::Cover
169
+ def ListEntitiesDateResponse.entity_class = Entities::Date
170
+ def ListEntitiesDateTimeResponse.entity_class = Entities::DateTime
171
+ def ListEntitiesFanResponse.entity_class = Entities::Fan
172
+ def ListEntitiesLightResponse.entity_class = Entities::Light
173
+ def ListEntitiesLockResponse.entity_class = Entities::Lock
174
+ def ListEntitiesNumberResponse.entity_class = Entities::Number
175
+ def ListEntitiesSelectResponse.entity_class = Entities::Select
176
+ def ListEntitiesSensorResponse.entity_class = Entities::Sensor
177
+ def ListEntitiesSwitchResponse.entity_class = Entities::Switch
178
+ def ListEntitiesTextResponse.entity_class = Entities::Text
179
+ def ListEntitiesTextSensorResponse.entity_class = Entities::TextSensor
180
+ def ListEntitiesTimeResponse.entity_class = Entities::Time
181
+ end
182
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Cli
5
+ module Colors
6
+ ANSI_COLOR_MAP = {
7
+ 30 => Curses::COLOR_BLACK,
8
+ 31 => Curses::COLOR_RED,
9
+ 32 => Curses::COLOR_GREEN,
10
+ 33 => Curses::COLOR_YELLOW,
11
+ 34 => Curses::COLOR_BLUE,
12
+ 35 => Curses::COLOR_MAGENTA,
13
+ 36 => Curses::COLOR_CYAN,
14
+ 37 => Curses::COLOR_WHITE
15
+ }.freeze
16
+
17
+ # Regex to match ANSI color codes (like \e[31m)
18
+ ANSI_ESCAPE_REGEX = /\e\[(\d+(?:;\d+)*)m/
19
+
20
+ private
21
+
22
+ def parse_sgr_codes(codes)
23
+ color = nil
24
+ attr = Curses::A_NORMAL
25
+
26
+ codes.each do |code|
27
+ code = code.to_i
28
+ if ANSI_COLOR_MAP.key?(code)
29
+ color = ANSI_COLOR_MAP[code]
30
+ elsif code == 1
31
+ attr |= Curses::A_BOLD
32
+ elsif code == 4
33
+ attr |= Curses::A_UNDERLINE
34
+ elsif code.zero?
35
+ attr = Curses::A_NORMAL
36
+ color = nil
37
+ end
38
+ end
39
+
40
+ [color, attr]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require "uri"
5
+
6
+ require "esphome/dashboard"
7
+
8
+ module ESPHome
9
+ module Cli
10
+ module Completion
11
+ class << self
12
+ def run(prefix: ARGV[1].to_s,
13
+ comp_line: ENV["COMP_LINE"],
14
+ multi: false)
15
+ argv = Shellwords.split(comp_line || "")[1..]
16
+ dashboard_uri, argv = extract_dashboard_uri(argv)
17
+ return if argv.length > 1 && !multi
18
+
19
+ dashboard = ESPHome::Dashboard.new(dashboard_uri)
20
+ puts dashboard.devices
21
+ .filter_map { |device| device["name"] }
22
+ .select { |name| name.start_with?(prefix) }
23
+ .sort
24
+ rescue HTTPX::Error, IOError, SocketError, SystemCallError, Timeout::Error
25
+ nil
26
+ end
27
+
28
+ private
29
+
30
+ def extract_dashboard_uri(argv)
31
+ argv ||= []
32
+ dashboard_uri = Dashboard::DEFAULT_URI
33
+ if argv[0]
34
+ uri = URI.parse(argv[0])
35
+ if uri.scheme
36
+ dashboard_uri = uri.to_s
37
+ argv = argv[1..]
38
+ end
39
+ end
40
+
41
+ [dashboard_uri, argv]
42
+ rescue URI::InvalidURIError
43
+ [dashboard_uri, argv]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Cli
5
+ module Entities
6
+ class Button < Entity
7
+ def formatted_state
8
+ "PRESS"
9
+ end
10
+
11
+ def activate
12
+ cli.info("Pressing #{object_id_}")
13
+ press
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "form"
4
+ require_relative "subfields"
5
+
6
+ module ESPHome
7
+ module Cli
8
+ module Entities
9
+ class Climate < Subfields
10
+ include FormPrompt
11
+
12
+ private
13
+
14
+ def subfields
15
+ items = [:mode]
16
+ if two_point_target_temperature?
17
+ items << :target_temperature_low
18
+ items << :target_temperature_high
19
+ else
20
+ items << :target_temperature
21
+ end
22
+ items << :fan_mode unless supported_fan_modes.empty?
23
+ items << :swing_mode unless supported_swing_modes.empty?
24
+ items << :preset unless supported_presets.empty?
25
+ items << :target_humidity if target_humidity?
26
+ items
27
+ end
28
+
29
+ def subfield_segment_indexes
30
+ segment_index = 0
31
+ indexes = { mode: segment_index }
32
+
33
+ segment_index += 1 if action?
34
+ segment_index += 1 if current_temperature?
35
+
36
+ segment_index += 1
37
+ if two_point_target_temperature?
38
+ indexes[:target_temperature_low] = segment_index
39
+ else
40
+ indexes[:target_temperature] = segment_index
41
+ end
42
+ if two_point_target_temperature?
43
+ segment_index += 1 # literal "-"
44
+ segment_index += 1
45
+ indexes[:target_temperature_high] = segment_index
46
+ end
47
+
48
+ unless supported_fan_modes.empty?
49
+ segment_index += 1
50
+ indexes[:fan_mode] = segment_index
51
+ end
52
+
53
+ unless supported_swing_modes.empty?
54
+ segment_index += 1
55
+ indexes[:swing_mode] = segment_index
56
+ end
57
+
58
+ unless supported_presets.empty?
59
+ segment_index += 1
60
+ indexes[:preset] = segment_index
61
+ end
62
+
63
+ segment_index += 1 if current_humidity?
64
+ segment_index += 1 if current_humidity? && target_humidity?
65
+ indexes[:target_humidity] = segment_index + 1 if target_humidity?
66
+ indexes
67
+ end
68
+
69
+ def highlighted_segment_parts(idx, segment)
70
+ selected_subfield = active_subfield
71
+
72
+ if idx == highlighted_segment_index && selected_subfield == :fan_mode
73
+ value = (fan_mode || "-").to_s
74
+ [formatted_fan_segment.delete_suffix(value), value, ""]
75
+ elsif idx == highlighted_segment_index && selected_subfield == :swing_mode
76
+ value = (swing_mode || "-").to_s
77
+ [formatted_swing_segment.delete_suffix(value), value, ""]
78
+ else
79
+ ["", segment, ""]
80
+ end
81
+ end
82
+
83
+ def activate_mode
84
+ choice = prompt_menu("Mode", supported_modes.map(&:to_s), state&.to_s)
85
+ return unless choice
86
+
87
+ cli.info("Setting #{object_id_} mode to #{choice}")
88
+ change_mode(choice)
89
+ end
90
+
91
+ def activate_fan_mode
92
+ choice = prompt_menu("Fan Mode", supported_fan_modes.map(&:to_s), fan_mode&.to_s)
93
+ return unless choice
94
+
95
+ cli.info("Setting #{object_id_} fan mode to #{choice}")
96
+ change_fan_mode(choice)
97
+ end
98
+
99
+ def activate_swing_mode
100
+ choice = prompt_menu("Swing Mode", supported_swing_modes.map(&:to_s), swing_mode&.to_s)
101
+ return unless choice
102
+
103
+ cli.info("Setting #{object_id_} swing mode to #{choice}")
104
+ change_swing_mode(choice)
105
+ end
106
+
107
+ def activate_preset
108
+ options = supported_presets.map(&:to_s)
109
+ choice = prompt_menu("Preset", options, preset&.to_s)
110
+ return unless choice
111
+
112
+ cli.info("Setting #{object_id_} preset to #{choice}")
113
+ change_preset(choice)
114
+ end
115
+
116
+ def activate_target_temperature
117
+ value = prompt_number("Target Temperature",
118
+ initial_value: target_temperature,
119
+ suffix: unit_symbol)
120
+ return unless value
121
+
122
+ cli.info("Setting #{object_id_} target to #{format_temperature(value)}")
123
+ change_target_temperature(value)
124
+ end
125
+
126
+ def activate_target_temperature_low
127
+ value = prompt_number("Target Low",
128
+ initial_value: target_temperature_low,
129
+ suffix: unit_symbol)
130
+ return unless value
131
+
132
+ cli.info("Setting #{object_id_} target low to #{format_temperature(value)}")
133
+ change_target_temperature(low: value)
134
+ end
135
+
136
+ def activate_target_temperature_high
137
+ value = prompt_number("Target High",
138
+ initial_value: target_temperature_high,
139
+ suffix: unit_symbol)
140
+ return unless value
141
+
142
+ cli.info("Setting #{object_id_} target high to #{format_temperature(value)}")
143
+ change_target_temperature(high: value)
144
+ end
145
+
146
+ def activate_target_humidity
147
+ value = prompt_number("Target Humidity",
148
+ initial_value: target_humidity,
149
+ suffix: "%RH",
150
+ decimals: 0,
151
+ range: visual_humidity_range)
152
+ return unless value
153
+
154
+ cli.info("Setting #{object_id_} target humidity to #{format_humidity(value)}")
155
+ change_target_humidity(value)
156
+ end
157
+
158
+ def prompt_number(title, initial_value:, suffix:, decimals: nil, range: nil)
159
+ decimals ||= temperature_decimals
160
+ initial_value = format("%.#{decimals}f", initial_value) if initial_value
161
+ value = prompt_form(title,
162
+ initial_value:,
163
+ suffix:,
164
+ field_width: number_field_width(range, decimals))
165
+ return unless value
166
+
167
+ normalize_number(value, range:, decimals:)
168
+ end
169
+
170
+ def normalize_number(value, range:, decimals:)
171
+ number = Float(value)
172
+ number = number.round(decimals)
173
+ return number unless range
174
+
175
+ number.clamp(range)
176
+ rescue ArgumentError
177
+ cli.error("Invalid number: #{value}")
178
+ nil
179
+ end
180
+
181
+ def number_field_width(range, decimals)
182
+ range ||= visual_temperature_range
183
+ [
184
+ number_display_width(range.begin, decimals),
185
+ number_display_width(range.end, decimals),
186
+ 6
187
+ ].max
188
+ end
189
+
190
+ def number_display_width(number, decimals)
191
+ format("%.#{decimals}f", number).length
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "form"
4
+ require_relative "subfields"
5
+
6
+ module ESPHome
7
+ module Cli
8
+ module Entities
9
+ class Cover < Subfields
10
+ include FormPrompt
11
+
12
+ VERBS = {
13
+ open: "Opening",
14
+ close: "Closing",
15
+ stop: "Stopping"
16
+ }.freeze
17
+ private_constant :VERBS
18
+
19
+ private
20
+
21
+ def subfields
22
+ items = []
23
+ items << :position if position?
24
+ items << :tilt if tilt?
25
+ items << :current_operation
26
+ items
27
+ end
28
+
29
+ def subfield_segment_indexes
30
+ segment_index = 0
31
+ indexes = {}
32
+ indexes[:position] = segment_index if position?
33
+ if tilt?
34
+ segment_index += 2
35
+ indexes[:tilt] = segment_index
36
+ end
37
+ segment_index += 1
38
+ indexes[:current_operation] = segment_index
39
+ indexes
40
+ end
41
+
42
+ def highlighted_segment_parts(idx, segment)
43
+ return ["", segment, ""] unless idx == highlighted_segment_index && active_subfield == :current_operation
44
+
45
+ value = (current_operation || "-").to_s
46
+ ["(", value, ")"]
47
+ end
48
+
49
+ def activate_position
50
+ value = prompt_percentage("Position", position)
51
+ return if value.nil?
52
+
53
+ cli.info("Setting #{object_id_} position to #{percentage_label(value)}")
54
+ command(position: value)
55
+ end
56
+
57
+ def activate_tilt
58
+ value = prompt_percentage("Tilt", tilt)
59
+ return if value.nil?
60
+
61
+ cli.info("Setting #{object_id_} tilt to #{percentage_label(value)}")
62
+ command(tilt: value)
63
+ end
64
+
65
+ def activate_current_operation
66
+ choice = prompt_menu("Operation", operation_options, current_operation_option)
67
+ return unless choice
68
+
69
+ choice = choice.to_sym
70
+ cli.info("#{VERBS[choice]} #{object_id_}")
71
+ __send__(choice)
72
+ end
73
+
74
+ def prompt_percentage(title, value)
75
+ initial_value = format("%d", (value * 100).round) if value.finite?
76
+ choice = prompt_form(title,
77
+ initial_value:,
78
+ suffix: "%",
79
+ field_width: 3)
80
+ return unless choice
81
+
82
+ normalize_percentage(choice)
83
+ end
84
+
85
+ def percentage_label(value)
86
+ "#{(value * 100).round}%"
87
+ end
88
+
89
+ def normalize_percentage(value)
90
+ value = Float(value).round.clamp(0, 100)
91
+ value / 100.0
92
+ rescue ArgumentError
93
+ cli.error("Invalid percentage: #{value}")
94
+ nil
95
+ end
96
+
97
+ def operation_options
98
+ options = %w[open close]
99
+ options << "stop" if stop?
100
+ options
101
+ end
102
+
103
+ def current_operation_option
104
+ case current_operation
105
+ when :opening
106
+ "open"
107
+ when :closing
108
+ "close"
109
+ when :idle
110
+ "stop" if stop?
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "form"
4
+
5
+ module ESPHome
6
+ module Cli
7
+ module Entities
8
+ class Date < Form
9
+ def length_range
10
+ 3..11
11
+ end
12
+
13
+ def command(value)
14
+ begin
15
+ value = Date.parse(value)
16
+ rescue ArgumentError
17
+ cli.error("Invalid date: #{value}")
18
+ return
19
+ end
20
+
21
+ cli.info("Setting #{object_id_} to #{value.iso8601}")
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ require_relative "form"
6
+
7
+ module ESPHome
8
+ module Cli
9
+ module Entities
10
+ class DateTime < Form
11
+ def length_range
12
+ 25..25
13
+ end
14
+
15
+ def command(value)
16
+ begin
17
+ value = Time.parse(value)
18
+ rescue ArgumentError
19
+ cli.error("Invalid timestamp: #{value}")
20
+ return
21
+ end
22
+
23
+ cli.info("Setting #{object_id_} to #{value.iso8601}")
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end