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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "menu"
|
|
4
|
+
|
|
5
|
+
module ESPHome
|
|
6
|
+
module Cli
|
|
7
|
+
module Entities
|
|
8
|
+
module FormPrompt
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def prompt_form(title, initial_value:, suffix:, field_width:)
|
|
12
|
+
field, form, win = build_form_dialog(title, suffix, field_width)
|
|
13
|
+
return unless win
|
|
14
|
+
|
|
15
|
+
field.set_buffer(0, initial_value) if initial_value
|
|
16
|
+
form.post
|
|
17
|
+
win.box(0, 0)
|
|
18
|
+
win.setpos(0, 2)
|
|
19
|
+
safe_addstr(win, " #{title} ")
|
|
20
|
+
if suffix
|
|
21
|
+
win.setpos(1, field_width + 3)
|
|
22
|
+
safe_addstr(win, suffix)
|
|
23
|
+
end
|
|
24
|
+
win.setpos(1, 2)
|
|
25
|
+
win.refresh
|
|
26
|
+
|
|
27
|
+
loop do
|
|
28
|
+
case (ch = win.getch)
|
|
29
|
+
when Curses::KEY_RIGHT
|
|
30
|
+
form.driver(Curses::REQ_NEXT_CHAR)
|
|
31
|
+
when Curses::KEY_LEFT
|
|
32
|
+
form.driver(Curses::REQ_PREV_CHAR)
|
|
33
|
+
when Curses::KEY_BACKSPACE
|
|
34
|
+
form.driver(Curses::REQ_DEL_PREV)
|
|
35
|
+
when Curses::KEY_DC
|
|
36
|
+
form.driver(Curses::REQ_DEL_CHAR)
|
|
37
|
+
when "\n".ord
|
|
38
|
+
form.driver(Curses::REQ_VALIDATION)
|
|
39
|
+
value = field.buffer(0).strip
|
|
40
|
+
return nil if value.empty?
|
|
41
|
+
|
|
42
|
+
return value
|
|
43
|
+
when 27, Curses::KEY_RESIZE
|
|
44
|
+
return nil
|
|
45
|
+
when nil
|
|
46
|
+
next
|
|
47
|
+
else
|
|
48
|
+
form.driver(ch)
|
|
49
|
+
end
|
|
50
|
+
rescue Curses::RequestDeniedError, Curses::UnknownCommandError
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
ensure
|
|
54
|
+
form&.unpost
|
|
55
|
+
win&.close
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_form_dialog(title, suffix, field_width)
|
|
59
|
+
width = field_width + 4 + (suffix ? suffix.length + 1 : 0)
|
|
60
|
+
win = build_dialog_window(3, width, title)
|
|
61
|
+
return [nil, nil, nil] unless win
|
|
62
|
+
|
|
63
|
+
field = Curses::Field.new(1, field_width, 0, 0, 0, 0)
|
|
64
|
+
field.set_back(Curses::A_UNDERLINE)
|
|
65
|
+
field.opts_off(Curses::O_AUTOSKIP)
|
|
66
|
+
form = Curses::Form.new([field])
|
|
67
|
+
form.set_win(win)
|
|
68
|
+
form.set_sub(win.derwin(1, field_width, 1, 2))
|
|
69
|
+
[field, form, win]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class Form < Entity
|
|
74
|
+
include MenuPrompt
|
|
75
|
+
include FormPrompt
|
|
76
|
+
|
|
77
|
+
def initialize(...)
|
|
78
|
+
super
|
|
79
|
+
|
|
80
|
+
@max_length = length_range&.end || 10
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def length_range
|
|
84
|
+
0..10
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def suffix = nil
|
|
88
|
+
|
|
89
|
+
def initial_value = nil
|
|
90
|
+
|
|
91
|
+
def activate
|
|
92
|
+
value = prompt_form(form_title,
|
|
93
|
+
initial_value:,
|
|
94
|
+
suffix:,
|
|
95
|
+
field_width:)
|
|
96
|
+
command(value) if value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def form_title
|
|
102
|
+
name
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def field_width
|
|
106
|
+
@max_length
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "menu"
|
|
4
|
+
|
|
5
|
+
module ESPHome
|
|
6
|
+
module Cli
|
|
7
|
+
module Entities
|
|
8
|
+
class Lock < Menu
|
|
9
|
+
def options
|
|
10
|
+
supports_open? ? %w[lock unlock open] : %w[lock unlock]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def command(command)
|
|
14
|
+
cli.info("#{command}ing #{object_id_}")
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ESPHome
|
|
4
|
+
module Cli
|
|
5
|
+
module Entities
|
|
6
|
+
module MenuPrompt
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def prompt_menu(title, options, current = nil)
|
|
10
|
+
content_width = options.map(&:length).max + 4
|
|
11
|
+
win = build_dialog_window(options.length + 2, content_width, title)
|
|
12
|
+
return unless win
|
|
13
|
+
|
|
14
|
+
index = options.index(current) || 0
|
|
15
|
+
loop do
|
|
16
|
+
render_select_options(win, title, options, index)
|
|
17
|
+
|
|
18
|
+
case win.getch
|
|
19
|
+
when Curses::Key::UP
|
|
20
|
+
index = (index - 1) % options.length
|
|
21
|
+
when Curses::Key::DOWN
|
|
22
|
+
index = (index + 1) % options.length
|
|
23
|
+
when "\n".ord
|
|
24
|
+
return options[index]
|
|
25
|
+
when 27, Curses::KEY_RESIZE
|
|
26
|
+
return nil
|
|
27
|
+
when nil
|
|
28
|
+
next
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
ensure
|
|
32
|
+
win&.close
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_select_options(win, title, options, index)
|
|
36
|
+
win.clear
|
|
37
|
+
win.box(0, 0)
|
|
38
|
+
win.setpos(0, 2)
|
|
39
|
+
safe_addstr(win, " #{title} ")
|
|
40
|
+
options.each_with_index do |option, idx|
|
|
41
|
+
win.setpos(idx + 1, 2)
|
|
42
|
+
if idx == index
|
|
43
|
+
win.attron(Curses::A_REVERSE) { safe_addstr(win, option) }
|
|
44
|
+
else
|
|
45
|
+
safe_addstr(win, option)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
win.refresh
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_dialog_window(height, width, title)
|
|
52
|
+
root = cli.win
|
|
53
|
+
return unless root
|
|
54
|
+
|
|
55
|
+
width = [width, title.length + 6].max
|
|
56
|
+
return if width > root.maxx || height > root.maxy
|
|
57
|
+
|
|
58
|
+
top = (index + Monitor::HEADER_ROWS).clamp(0, root.maxy - height)
|
|
59
|
+
left = (cli.name_width + 3).clamp(0, root.maxx - width)
|
|
60
|
+
win = Curses::Window.new(height, width, top, left)
|
|
61
|
+
win.keypad = true
|
|
62
|
+
win.timeout = 100
|
|
63
|
+
win
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Menu < Entity
|
|
68
|
+
include MenuPrompt
|
|
69
|
+
|
|
70
|
+
def activate
|
|
71
|
+
choice = prompt_menu(menu_title, options)
|
|
72
|
+
command(choice) if choice
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def menu_title
|
|
78
|
+
name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "form"
|
|
4
|
+
|
|
5
|
+
module ESPHome
|
|
6
|
+
module Cli
|
|
7
|
+
module Entities
|
|
8
|
+
class Number < Form
|
|
9
|
+
def length_range
|
|
10
|
+
max_length = range&.end ? range.end.to_s.length : 10
|
|
11
|
+
max_length += accuracy_decimals + 1 if accuracy_decimals.positive?
|
|
12
|
+
1..max_length
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def suffix
|
|
16
|
+
unit_of_measurement
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initial_value
|
|
20
|
+
format("%.#{accuracy_decimals}f", state) if state
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def command(value)
|
|
24
|
+
begin
|
|
25
|
+
value = Float(value)
|
|
26
|
+
rescue ArgumentError
|
|
27
|
+
cli.error("Invalid number: #{value}")
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
cli.info("Setting #{object_id_} to #{formatted_state(value)}")
|
|
32
|
+
set(value)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "menu"
|
|
4
|
+
|
|
5
|
+
module ESPHome
|
|
6
|
+
module Cli
|
|
7
|
+
module Entities
|
|
8
|
+
class Subfields < Entity
|
|
9
|
+
include MenuPrompt
|
|
10
|
+
|
|
11
|
+
def initialize(...)
|
|
12
|
+
super
|
|
13
|
+
|
|
14
|
+
@selected_subfield = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def move_left
|
|
18
|
+
return if subfields.size <= 1
|
|
19
|
+
|
|
20
|
+
@selected_subfield = (@selected_subfield - 1) % subfields.size
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def move_right
|
|
24
|
+
return if subfields.size <= 1
|
|
25
|
+
|
|
26
|
+
@selected_subfield = (@selected_subfield + 1) % subfields.size
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def activate
|
|
30
|
+
__send__(:"activate_#{active_subfield}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def print_state(win, active:)
|
|
36
|
+
formatted_segments.each_with_index do |segment, idx|
|
|
37
|
+
safe_addstr(win, " ") unless idx.zero?
|
|
38
|
+
if active && idx == highlighted_segment_index
|
|
39
|
+
prefix, value, suffix = highlighted_segment_parts(idx, segment)
|
|
40
|
+
safe_addstr(win, prefix)
|
|
41
|
+
win.attron(Curses::A_REVERSE) { safe_addstr(win, value) }
|
|
42
|
+
safe_addstr(win, suffix)
|
|
43
|
+
else
|
|
44
|
+
safe_addstr(win, segment)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def active_subfield
|
|
50
|
+
subfields.fetch(@selected_subfield, subfields.first)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def highlighted_segment_index
|
|
54
|
+
subfield_segment_indexes.fetch(active_subfield)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def highlighted_segment_parts(_idx, segment)
|
|
58
|
+
["", segment, ""]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "menu"
|
|
4
|
+
|
|
5
|
+
module ESPHome
|
|
6
|
+
module Cli
|
|
7
|
+
module Entities
|
|
8
|
+
class Switch < Menu
|
|
9
|
+
def options = %w[on off]
|
|
10
|
+
|
|
11
|
+
def command(command)
|
|
12
|
+
cli.info("Turning #{object_id_} #{command}")
|
|
13
|
+
__send__(command.to_sym)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "form"
|
|
4
|
+
|
|
5
|
+
module ESPHome
|
|
6
|
+
module Cli
|
|
7
|
+
module Entities
|
|
8
|
+
class Time < Form
|
|
9
|
+
def length_range
|
|
10
|
+
25..25
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def command(value)
|
|
14
|
+
if defined?(ActiveSupport::Duration)
|
|
15
|
+
begin
|
|
16
|
+
value = ActiveSupport::Duration.parse(value)
|
|
17
|
+
rescue ArgumentError
|
|
18
|
+
# ignore; try to parse another way
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
if value.is_a?(String)
|
|
24
|
+
value = value.split(":").map { |v| Integer(v) }
|
|
25
|
+
raise ArgumentError if value.length > 3
|
|
26
|
+
|
|
27
|
+
value.unshift(0) while value.length < 3
|
|
28
|
+
end
|
|
29
|
+
rescue ArgumentError
|
|
30
|
+
cli.error("Invalid time: #{value}")
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
cli.info("Setting #{object_id_} to #{value.iso8601}")
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module ESPHome
|
|
6
|
+
module Cli
|
|
7
|
+
class Entity < SimpleDelegator
|
|
8
|
+
attr_reader :cli, :index
|
|
9
|
+
|
|
10
|
+
def initialize(cli, entity, index)
|
|
11
|
+
super(entity)
|
|
12
|
+
|
|
13
|
+
@cli = cli
|
|
14
|
+
@index = index
|
|
15
|
+
touch
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def touch = @last_update = Time.now
|
|
19
|
+
def move_left = nil
|
|
20
|
+
def move_right = nil
|
|
21
|
+
|
|
22
|
+
def print(win, clear_line: true, active: false)
|
|
23
|
+
row = @index + Monitor::HEADER_ROWS
|
|
24
|
+
return if row >= win.maxy
|
|
25
|
+
|
|
26
|
+
win.setpos(row, 0)
|
|
27
|
+
win.clrtoeol if clear_line
|
|
28
|
+
safe_addstr(win, "#{name.ljust(cli.name_width)} : ")
|
|
29
|
+
print_state(win, active:)
|
|
30
|
+
|
|
31
|
+
return unless __getobj__.is_a?(ESPHome::Entity::HasState)
|
|
32
|
+
|
|
33
|
+
timestamp = "[#{@last_update.strftime("%H:%M:%S.%L")}]"
|
|
34
|
+
return if timestamp.length >= win.maxx
|
|
35
|
+
|
|
36
|
+
pos = [win.cury, win.curx]
|
|
37
|
+
win.setpos(row, win.maxx - timestamp.length)
|
|
38
|
+
safe_addstr(win, timestamp)
|
|
39
|
+
win.setpos(*pos) if pos[1] < win.maxx
|
|
40
|
+
rescue Curses::Error
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def print_state(win, active:)
|
|
47
|
+
state = formatted_state
|
|
48
|
+
if active
|
|
49
|
+
win.attron(Curses::A_REVERSE) { safe_addstr(win, state) }
|
|
50
|
+
else
|
|
51
|
+
safe_addstr(win, state)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def safe_addstr(win, string)
|
|
56
|
+
return if string.nil? || string.empty?
|
|
57
|
+
|
|
58
|
+
remaining = win.maxx - win.curx
|
|
59
|
+
return if remaining <= 0
|
|
60
|
+
|
|
61
|
+
win.addstr(string[0, remaining])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
require_relative "entities/button"
|
|
68
|
+
require_relative "entities/climate"
|
|
69
|
+
require_relative "entities/cover"
|
|
70
|
+
require_relative "entities/date"
|
|
71
|
+
require_relative "entities/date_time"
|
|
72
|
+
require_relative "entities/lock"
|
|
73
|
+
require_relative "entities/number"
|
|
74
|
+
require_relative "entities/select"
|
|
75
|
+
require_relative "entities/switch"
|
|
76
|
+
require_relative "entities/subfields"
|
|
77
|
+
require_relative "entities/text"
|
|
78
|
+
require_relative "entities/time"
|