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,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Climate < Entity
6
+ include HasState
7
+
8
+ BASE_FAN_MODES = Api::ClimateCommandRequest.descriptor.lookup("fan_mode")
9
+ .subtype
10
+ .map { |value| value.name[12..].downcase.to_sym }
11
+ .freeze
12
+ private_constant :BASE_FAN_MODES
13
+ BASE_PRESETS = Api::ClimateCommandRequest.descriptor.lookup("preset")
14
+ .subtype
15
+ .map { |value, _number| value.to_s }
16
+ .reject { |value| value == "CLIMATE_PRESET_NONE" }
17
+ .map { |value| value[15..].downcase.to_sym }
18
+ .freeze
19
+ private_constant :BASE_PRESETS
20
+ UNIT_SYMBOLS = {
21
+ celsius: "°C",
22
+ fahrenheit: "°F",
23
+ kelvin: "K"
24
+ }.freeze
25
+
26
+ module Features
27
+ SUPPORTS_CURRENT_TEMPERATURE = 1 << 0
28
+ SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 1
29
+ REQUIRES_TWO_POINT_TARGET_TEMPERATURE = 1 << 2
30
+ SUPPORTS_CURRENT_HUMIDITY = 1 << 3
31
+ SUPPORTS_TARGET_HUMIDITY = 1 << 4
32
+ SUPPORTS_ACTION = 1 << 5
33
+ end
34
+
35
+ attr_reader :supported_modes,
36
+ :visual_temperature_range,
37
+ :visual_target_temperature_step,
38
+ :supported_fan_modes,
39
+ :supported_swing_modes,
40
+ :supported_presets,
41
+ :visual_humidity_range,
42
+ :current_temperature,
43
+ :target_temperature,
44
+ :target_temperature_low,
45
+ :target_temperature_high,
46
+ :action,
47
+ :fan_mode,
48
+ :swing_mode,
49
+ :preset,
50
+ :current_humidity,
51
+ :target_humidity,
52
+ :temperature_unit
53
+
54
+ def initialize(_device, list_entities_response)
55
+ super
56
+
57
+ if list_entities_response.feature_flags
58
+ @supports_current_temperature = list_entities_response.feature_flags.anybits?(Features::SUPPORTS_CURRENT_TEMPERATURE)
59
+ @supports_two_point_target_temperature = list_entities_response.feature_flags.anybits?(Features::SUPPORTS_TWO_POINT_TARGET_TEMPERATURE)
60
+ @supports_current_humidity = list_entities_response.feature_flags.anybits?(Features::SUPPORTS_CURRENT_HUMIDITY)
61
+ @supports_target_humidity = list_entities_response.feature_flags.anybits?(Features::SUPPORTS_TARGET_HUMIDITY)
62
+ @supports_action = list_entities_response.feature_flags.anybits?(Features::SUPPORTS_ACTION)
63
+ else
64
+ @supports_current_temperature = list_entities_response.supports_current_temperature
65
+ @supports_two_point_target_temperature = list_entities_response.supports_two_point_target_temperature
66
+ @supports_current_humidity = list_entities_response.supports_current_humidity
67
+ @supports_target_humidity = list_entities_response.supports_target_humidity
68
+ @supports_action = list_entities_response.supports_action
69
+ end
70
+ @supported_modes = list_entities_response.supported_modes.map { |m| m[13..].downcase.to_sym }.freeze
71
+ @visual_temperature_range = Range.new(list_entities_response.visual_min_temperature,
72
+ list_entities_response.visual_max_temperature)
73
+ @visual_target_temperature_step = list_entities_response.visual_target_temperature_step
74
+ @supported_fan_modes = list_entities_response.supported_fan_modes.map { |m| m[12..].downcase.to_sym }
75
+ @supported_swing_modes = list_entities_response.supported_swing_modes.map { |m| m[14..].downcase.to_sym }.freeze
76
+ @supported_fan_modes.concat(list_entities_response.supported_custom_fan_modes.map(&:to_sym))
77
+ @supported_fan_modes.freeze
78
+ @supported_presets = list_entities_response.supported_presets.map { |p| p[15..].downcase.to_sym }
79
+ if list_entities_response.legacy_supports_away && !@supported_presets.include?(:away)
80
+ @supported_presets.push(:away)
81
+ end
82
+ @supported_presets.concat(list_entities_response.supported_custom_presets.map(&:to_sym))
83
+ @supported_presets.freeze
84
+ @visual_humidity_range = Range.new(list_entities_response.visual_min_humidity,
85
+ list_entities_response.visual_max_humidity)
86
+ @temperature_unit = list_entities_response.temperature_unit&.[](17..)&.downcase&.to_sym || :celsius
87
+ end
88
+
89
+ def current_temperature?
90
+ @supports_current_temperature
91
+ end
92
+
93
+ def two_point_target_temperature?
94
+ @supports_two_point_target_temperature
95
+ end
96
+
97
+ def action?
98
+ @supports_action
99
+ end
100
+
101
+ def current_humidity?
102
+ @supports_current_humidity
103
+ end
104
+
105
+ def target_humidity?
106
+ @supports_target_humidity
107
+ end
108
+
109
+ def update(state_response)
110
+ @state = state_response.mode[13..].downcase.to_sym
111
+ @current_temperature = state_response.current_temperature if current_temperature?
112
+ if two_point_target_temperature?
113
+ @target_temperature_low = state_response.target_temperature_low
114
+ @target_temperature_high = state_response.target_temperature_high
115
+ else
116
+ @target_temperature = state_response.target_temperature
117
+ end
118
+ @action = state_response.action[15..].downcase.to_sym if action?
119
+ @fan_mode = if state_response.custom_fan_mode.empty?
120
+ state_response.fan_mode[12..].downcase.to_sym
121
+ else
122
+ state_response.custom_fan_mode.downcase.to_sym
123
+ end
124
+ @swing_mode = state_response.swing_mode[14..].downcase.to_sym
125
+ @preset = if !state_response.custom_preset.empty?
126
+ state_response.custom_preset.to_sym
127
+ elsif state_response.unused_legacy_away
128
+ :away
129
+ elsif state_response.preset == :CLIMATE_PRESET_NONE
130
+ nil
131
+ else
132
+ state_response.preset[15..].downcase.to_sym
133
+ end
134
+ @current_humidity = state_response.current_humidity if current_humidity?
135
+ @target_humidity = state_response.target_humidity if target_humidity?
136
+ end
137
+
138
+ def formatted_state
139
+ formatted_segments.join(" ")
140
+ end
141
+
142
+ def formatted_segments
143
+ segments = [state.nil? ? "-" : state.to_s]
144
+ segments << "(#{action || "-"})" if action?
145
+ segments << "#{format_temperature(current_temperature)} /" if current_temperature?
146
+ if two_point_target_temperature?
147
+ segments << format_temperature(target_temperature_low)
148
+ segments << "-"
149
+ segments << format_temperature(target_temperature_high)
150
+ else
151
+ segments << format_temperature(target_temperature)
152
+ end
153
+ segments << "fan: #{fan_mode || "-"}" unless supported_fan_modes.empty?
154
+ segments << "swing: #{swing_mode || "-"}" unless supported_swing_modes.empty?
155
+ segments << formatted_preset_segment unless supported_presets.empty?
156
+ segments << "#{current_humidity || "-"} %RH" if current_humidity?
157
+ segments << "/" if current_humidity? && target_humidity?
158
+ segments << "#{target_humidity || "-"} %RH" if target_humidity?
159
+ segments
160
+ end
161
+
162
+ def formatted_fan_segment
163
+ "fan: #{fan_mode || "-"}"
164
+ end
165
+
166
+ def formatted_swing_segment
167
+ "swing: #{swing_mode || "-"}"
168
+ end
169
+
170
+ def formatted_preset_segment
171
+ preset ? preset.to_s : "<preset>"
172
+ end
173
+
174
+ def temperature_decimals
175
+ str = visual_target_temperature_step.to_s
176
+ return 0 unless str.include?(".")
177
+
178
+ str.split(".").last.sub(/0+\z/, "").length
179
+ end
180
+
181
+ def format_temperature(value)
182
+ return "-" if value.nil?
183
+
184
+ format("%.#{temperature_decimals}f #{unit_symbol}", value)
185
+ end
186
+
187
+ def format_humidity(value)
188
+ return "-" if value.nil?
189
+
190
+ format("%.0f %%RH", value)
191
+ end
192
+
193
+ def unit_symbol
194
+ UNIT_SYMBOLS[temperature_unit]
195
+ end
196
+
197
+ def change_mode(mode)
198
+ device.send(Api::ClimateCommandRequest.new(key:,
199
+ has_mode: true,
200
+ mode: :"CLIMATE_MODE_#{mode.to_s.upcase}"))
201
+ end
202
+
203
+ def change_fan_mode(mode)
204
+ mode = mode.to_sym
205
+ if BASE_FAN_MODES.include?(mode)
206
+ device.send(Api::ClimateCommandRequest.new(key:,
207
+ has_fan_mode: true,
208
+ fan_mode: :"CLIMATE_FAN_#{mode.to_s.upcase}"))
209
+ else
210
+ device.send(Api::ClimateCommandRequest.new(key:,
211
+ has_custom_fan_mode: true,
212
+ custom_fan_mode: mode.to_s))
213
+ end
214
+ end
215
+
216
+ def change_target_temperature(value = nil, low: nil, high: nil)
217
+ command = Api::ClimateCommandRequest.new(key:)
218
+ if low
219
+ command.has_target_temperature_low = true
220
+ command.target_temperature_low = low
221
+ end
222
+ if high
223
+ command.has_target_temperature_high = true
224
+ command.target_temperature_high = high
225
+ end
226
+ if value
227
+ command.has_target_temperature = true
228
+ command.target_temperature = value
229
+ end
230
+ device.send(command)
231
+ end
232
+
233
+ def change_swing_mode(mode)
234
+ device.send(Api::ClimateCommandRequest.new(key:,
235
+ has_swing_mode: true,
236
+ swing_mode: :"CLIMATE_SWING_#{mode.to_s.upcase}"))
237
+ end
238
+
239
+ def change_target_humidity(value)
240
+ device.send(Api::ClimateCommandRequest.new(key:,
241
+ has_target_humidity: true,
242
+ target_humidity: value))
243
+ end
244
+
245
+ def change_preset(value)
246
+ value = value.to_sym
247
+ if BASE_PRESETS.include?(value)
248
+ device.send(Api::ClimateCommandRequest.new(key:,
249
+ has_preset: true,
250
+ preset: :"CLIMATE_PRESET_#{value.to_s.upcase}"))
251
+ else
252
+ device.send(Api::ClimateCommandRequest.new(key:,
253
+ has_custom_preset: true,
254
+ custom_preset: value.to_s))
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Cover < Entity
6
+ include HasDeviceClass
7
+ include HasAssumedState
8
+
9
+ attr_reader :position, :tilt, :current_operation
10
+
11
+ def initialize(_device, list_entities_response)
12
+ super
13
+
14
+ @supports_position = list_entities_response.supports_position
15
+ @supports_stop = list_entities_response.supports_stop
16
+ @supports_tilt = list_entities_response.supports_tilt
17
+ @position = @tilt = @current_operation = nil
18
+ end
19
+
20
+ def position?
21
+ @supports_position
22
+ end
23
+
24
+ def tilt?
25
+ @supports_tilt
26
+ end
27
+
28
+ def stop?
29
+ @supports_stop
30
+ end
31
+
32
+ def formatted_state
33
+ formatted_segments.join(" ")
34
+ end
35
+
36
+ def formatted_segments
37
+ segments = [formatted_position_segment]
38
+ if tilt?
39
+ segments << "-"
40
+ segments << formatted_tilt_segment
41
+ end
42
+ segments << formatted_operation_segment
43
+ end
44
+
45
+ def formatted_position_segment
46
+ if position?
47
+ formatted_percentage_segment(position)
48
+ elsif position == 1.0 # rubocop:disable Lint/FloatComparison
49
+ "OPEN"
50
+ elsif position == 0.0
51
+ "CLOSED"
52
+ else
53
+ "-"
54
+ end
55
+ end
56
+
57
+ def formatted_tilt_segment
58
+ formatted_percentage_segment(tilt)
59
+ end
60
+
61
+ def formatted_operation_segment
62
+ "(#{current_operation || "-"})"
63
+ end
64
+
65
+ def update(state_response)
66
+ @position = state_response.position if state_response.position
67
+ @tilt = state_response.tilt if tilt?
68
+
69
+ @current_operation = if state_response.current_operation == :COVER_OPERATION_IDLE
70
+ :idle
71
+ else
72
+ state_response.current_operation[19..].downcase.to_sym
73
+ end
74
+ end
75
+
76
+ def command(position: nil, tilt: nil)
77
+ command = Api::CoverCommandRequest.new(key:)
78
+ if position
79
+ command.has_position = true
80
+ command.position = position
81
+ end
82
+ if tilt
83
+ command.has_tilt = true
84
+ command.tilt = tilt
85
+ end
86
+ device.send(command)
87
+ end
88
+
89
+ def open
90
+ device.send(Api::CoverCommandRequest.new(key:,
91
+ has_legacy_command: true,
92
+ legacy_command: :LEGACY_COVER_COMMAND_OPEN,
93
+ has_position: true,
94
+ position: 1.0))
95
+ end
96
+
97
+ def close
98
+ device.send(Api::CoverCommandRequest.new(key:,
99
+ has_legacy_command: true,
100
+ legacy_command: :LEGACY_COVER_COMMAND_CLOSE,
101
+ has_position: true,
102
+ position: 0.0))
103
+ end
104
+
105
+ def stop
106
+ device.send(Api::CoverCommandRequest.new(key:,
107
+ has_legacy_command: true,
108
+ legacy_command: :LEGACY_COVER_COMMAND_STOP,
109
+ stop: true))
110
+ end
111
+
112
+ private
113
+
114
+ def formatted_percentage_segment(value)
115
+ "#{if value.nil?
116
+ "_"
117
+ elsif value.finite?
118
+ (value * 100).round
119
+ else
120
+ value
121
+ end}%"
122
+ end
123
+
124
+ def inspection_vars
125
+ super + %i[position tilt current_operation]
126
+ end
127
+
128
+ def hideable?(var, val)
129
+ super ||
130
+ (var == :position && !position?) ||
131
+ (var == :tilt && !tilt?)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Date < Entity
6
+ include HasState
7
+
8
+ def update(state_response)
9
+ @state = if state_response.missing_state
10
+ nil
11
+ else
12
+ ::Date.new(state_response.year,
13
+ state_response.month,
14
+ state_response.day)
15
+ end
16
+ end
17
+
18
+ def command(state)
19
+ device.send(Api::DateCommandRequest.new(key:, year: state.year, month: state.month, day: state.day))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class DateTime < Entity
6
+ include HasState
7
+
8
+ def update(state_response)
9
+ @state = if state_response.missing_state
10
+ nil
11
+ else
12
+ ::Time.at(state_response.epoch_seconds)
13
+ end
14
+ end
15
+
16
+ def command(state)
17
+ device.send(Api::DateTimeCommandRequest.new(key:, epoch_seconds: state.to_i))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Fan < Entity
6
+ include HasState
7
+
8
+ attr_reader :speed_count,
9
+ :preset_modes,
10
+ :speed,
11
+ :direction,
12
+ :preset_mode
13
+
14
+ def initialize(_device, list_entities_response)
15
+ super
16
+
17
+ @supports_oscillation = list_entities_response.supports_oscillation
18
+ @supports_speed = list_entities_response.supports_speed
19
+ @speed_count = list_entities_response.supported_speed_count if speed?
20
+ @speed_count = 3 if speed_count.zero? && speed?
21
+ @preset_modes = list_entities_response.supported_preset_modes.map(&:freeze).freeze
22
+
23
+ @oscillating = @speed = @direction = @preset_mode = nil
24
+ end
25
+
26
+ def oscillation?
27
+ @supports_oscillation
28
+ end
29
+
30
+ def speed?
31
+ @supports_speed
32
+ end
33
+
34
+ def oscillating?
35
+ @oscillating
36
+ end
37
+
38
+ def formatted_state
39
+ result = case state
40
+ when nil then "-"
41
+ when true then "on"
42
+ else "off"
43
+ end
44
+ if speed?
45
+ result +=
46
+ if speed_count == 100
47
+ " #{speed || "-"}%"
48
+ else
49
+ " #{speed || "-"}/#{speed_count}"
50
+ end
51
+ end
52
+ result += " oscillating" if oscillating?
53
+ result += " reversed" if direction == :reverse
54
+ result += " (#{preset_mode})" if preset_mode
55
+ result
56
+ end
57
+
58
+ def update(state_response)
59
+ @state = state_response.state
60
+ @oscillating = state_response.oscillating
61
+ @speed = state_response.speed_level
62
+ if speed? && speed.nil?
63
+ @speed = { FAN_SPEED_LOW: 1,
64
+ FAN_SPEED_MEDIUM: 2,
65
+ FAN_SPEED_HIGH: 3 }[state_response.speed]
66
+ end
67
+ @direction = state_response.direction[14..].downcase.to_sym
68
+ @preset_mode = state_response.preset_mode.empty? ? nil : state_response.preset_mode
69
+ end
70
+
71
+ private
72
+
73
+ def inspection_vars
74
+ super + %i[oscillation? speed_count preset_modes speed oscillating? direction preset_mode]
75
+ end
76
+
77
+ def hideable?(var, val)
78
+ super ||
79
+ (var == :oscillation && !oscillation?) ||
80
+ (var == :speed_count && !speed?) ||
81
+ (var == :preset_modes && preset_modes.empty?) ||
82
+ (var == :speed && !speed?) ||
83
+ (var == :oscillating && !oscillating?) ||
84
+ (var == :direction && direction == :forward) ||
85
+ (var == :preset_mode && preset_mode.nil?)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ESPHome
4
+ module Entities
5
+ class Light < Entity
6
+ include HasState
7
+
8
+ attr_reader :supported_color_modes,
9
+ :mireds_range,
10
+ :effects,
11
+ :brightness,
12
+ :color_mode,
13
+ :color_brightness,
14
+ :color_temperature,
15
+ :red,
16
+ :green,
17
+ :blue,
18
+ :white,
19
+ :cold_white,
20
+ :warm_white,
21
+ :transition_length,
22
+ :flash_effect,
23
+ :effect
24
+
25
+ def initialize(_device, list_entities_response)
26
+ super
27
+
28
+ @supported_color_modes = list_entities_response.supported_color_modes.map { |mode| mode[11..].downcase.to_sym }
29
+ if @supported_color_modes.empty?
30
+ @supported_color_modes << :brightness if list_entities_response.legacy_supports_brightness
31
+ @supported_color_modes << :rgb if list_entities_response.legacy_supports_rgb
32
+ @supported_color_modes << :white if list_entities_response.legacy_supports_white
33
+ @supported_color_modes << :color_temperature if list_entities_response.legacy_supports_color_temperature
34
+ @supported_color_modes = [:on_off] if @supported_color_modes.empty?
35
+ end
36
+ @supported_color_modes = @supported_color_modes.to_set { |mode| transform_color_mode(mode) }.freeze
37
+
38
+ @mireds_range = Range.new(list_entities_response.min_mireds, list_entities_response.max_mireds)
39
+
40
+ @effects = list_entities_response.effects.map(&:freeze).freeze
41
+
42
+ @brightness = @color_temperature = @red = @green = @blue = @white = @cold_white = @warm_white = nil
43
+ end
44
+
45
+ def brightness?
46
+ @supported_color_modes.include?(:brightness)
47
+ end
48
+
49
+ def white?
50
+ @supported_color_modes.intersect?(%i[white ww rgbw rgbww])
51
+ end
52
+
53
+ def rgb?
54
+ @supported_color_modes.intersect?(%i[rgb rgbw rgb_color_temperature rgbww])
55
+ end
56
+
57
+ def color_temperature?
58
+ @supported_color_modes.intersect?(%i[color_temperature rgb_color_temperature])
59
+ end
60
+
61
+ def ww?
62
+ @supported_color_modes.intersect?(%i[ww rgbww])
63
+ end
64
+
65
+ def update(state_response)
66
+ @state = state_response.state
67
+ @brightness = state_response.brightness if brightness?
68
+ @color_mode = state_response.color_mode
69
+ if rgb?
70
+ @color_brightness = state_response.color_brightness
71
+ @red = state_response.red
72
+ @green = state_response.green
73
+ @blue = state_response.blue
74
+ end
75
+ @white = state_response.white if white?
76
+ @color_temperature = state_response.color_temperature if color_temperature?
77
+ if ww?
78
+ @cold_white = state_response.cold_white
79
+ @warm_white = state_response.warm_white
80
+ end
81
+ @effect = state_response.effect.empty? ? nil : state_response.effect
82
+ end
83
+
84
+ def formatted_state
85
+ result = case state
86
+ when nil then "-"
87
+ when true then "on"
88
+ else "off"
89
+ end
90
+ result += " #{brightness || color_brightness || "-"}%" if brightness?
91
+
92
+ if effect
93
+ result += effect
94
+ else
95
+ result += " #{color_temperature || "-"} mired" if color_temperature?
96
+
97
+ color = []
98
+ color.push(red, green, blue) if rgb?
99
+ color.push(white) if white?
100
+ color.push(cold_white, warm_white) if ww?
101
+ result += " (#{color.map { |c| c || "-" }.join(",")})" unless color.empty?
102
+ end
103
+
104
+ result += " @ #{transition_length} ms" if transition_length
105
+ result += " (flash)" if flash_effect
106
+
107
+ result
108
+ end
109
+
110
+ private
111
+
112
+ def transform_color_mode(mode)
113
+ case mode
114
+ when :cold_warm_white then :ww
115
+ when :rgb_white then :rgbw
116
+ when :rgb_cold_warm_white then :rgbww
117
+ else mode
118
+ end
119
+ end
120
+
121
+ def inspection_vars
122
+ super + %i[supported_color_modes
123
+ mireds_range
124
+ effects
125
+ brightness
126
+ color_mode
127
+ color_brightness
128
+ color_temperature
129
+ red
130
+ green
131
+ blue
132
+ white
133
+ cold_white
134
+ warm_white
135
+ transition_length
136
+ flash_effect
137
+ effect]
138
+ end
139
+
140
+ def hideable?(var, val)
141
+ super ||
142
+ (var == :supported_color_modes && supported_color_modes.size <= 1) ||
143
+ (var == :mireds_range && !color_temperature?) ||
144
+ (var == :effects && effects.empty?) ||
145
+ (var == :brightness && !brightness?) ||
146
+ (var == :color_mode && supported_color_modes.size <= 1) ||
147
+ (var == :color_brightness && !brightness? && !rgb?) ||
148
+ (var == :color_temperature && !color_temperature?) ||
149
+ (%i[red green blue].include?(var) && !rgb?) ||
150
+ (var == :white && !white?) ||
151
+ (%(cold_white warm_white).include?(var) && !ww?) ||
152
+ (var == :transition_length && !transition_length) ||
153
+ (var == :flash_effect && !flash_effect) ||
154
+ (var == :effect && effects.empty?)
155
+ end
156
+ end
157
+ end
158
+ end