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
data/lib/esphome/api.rb
ADDED
|
@@ -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,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
|