mqtt-homeassistant 0.1.6 → 1.0.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.
@@ -2,17 +2,489 @@
2
2
 
3
3
  require "json"
4
4
 
5
- require "mqtt/homie"
6
- require "mqtt/home_assistant/homie/device"
7
- require "mqtt/home_assistant/homie/node"
8
- require "mqtt/home_assistant/homie/property"
9
-
10
5
  module MQTT
11
6
  module HomeAssistant
12
- class << self
13
- ENTITY_CATEGORIES = %i[config diagnostic system].freeze
14
- DEVICE_CLASSES = {
15
- binary_sensor: %i[
7
+ SPECIAL_ATTRIBUTES = {
8
+ common: %i[
9
+ availability
10
+ availability_mode
11
+ availability_template
12
+ availability_topic
13
+ device
14
+ enabled_by_default
15
+ entity_category
16
+ entity_picture
17
+ icon
18
+ json_attributes_template
19
+ json_attributes_topic
20
+ name
21
+ object_id
22
+ optimistic
23
+ payload_available
24
+ payload_not_available
25
+ platform
26
+ qos
27
+ retain
28
+ unique_id
29
+ ].freeze,
30
+ availability: %i[
31
+ payload_available
32
+ payload_not_available
33
+ topic
34
+ value_template
35
+ ].freeze,
36
+ device: %i[
37
+ configuration_url
38
+ connections
39
+ hw_version
40
+ identifiers
41
+ manufacturer
42
+ model
43
+ model_id
44
+ name
45
+ serial_number
46
+ suggested_area
47
+ sw_version
48
+ via_device
49
+ ].freeze
50
+ }.freeze
51
+ KNOWN_ATTRIBUTES = {
52
+ binary_sensor: %i[
53
+ state_topic
54
+ device_class
55
+ expire_after
56
+ force_update
57
+ off_delay
58
+ payload_off
59
+ payload_on
60
+ ].freeze,
61
+ button: %i[
62
+ command_template
63
+ command_topic
64
+ device_class
65
+ payload_press
66
+ ].freeze,
67
+ climate: %i[
68
+ action_template
69
+ action_topic
70
+ current_humidity_template
71
+ current_humidity_topic
72
+ current_temperature_template
73
+ current_temperature_topic
74
+ fan_mode_command_template
75
+ fan_mode_command_topic
76
+ fan_mode_state_template
77
+ fan_mode_state_topic
78
+ fan_modes
79
+ humidity_range
80
+ initial
81
+ max_humidity
82
+ max_temp
83
+ min_humidity
84
+ min_temp
85
+ mode_command_template
86
+ mode_command_topic
87
+ mode_state_template
88
+ mode_state_topic
89
+ modes
90
+ payload_off
91
+ payload_on
92
+ power_command_template
93
+ power_command_topic
94
+ power_state_template
95
+ power_state_topic
96
+ precision
97
+ preset_mode_command_template
98
+ preset_mode_command_topic
99
+ preset_mode_state_topic
100
+ preset_mode_value_template
101
+ preset_modes
102
+ swing_mode_command_template
103
+ swing_mode_command_topic
104
+ swing_mode_state_template
105
+ swing_mode_state_topic
106
+ swing_modes
107
+ target_humidity_command_template
108
+ target_humidity_command_topic
109
+ target_humidity_state_template
110
+ target_humidity_state_topic
111
+ temp_range
112
+ temp_step
113
+ temperature_command_template
114
+ temperature_command_topic
115
+ temperature_high_command_template
116
+ temperature_high_command_topic
117
+ temperature_high_state_template
118
+ temperature_high_state_topic
119
+ temperature_low_command_template
120
+ temperature_low_command_topic
121
+ temperature_low_state_template
122
+ temperature_low_state_topic
123
+ temperature_state_template
124
+ temperature_state_topic
125
+ temperature_unit
126
+ value_template
127
+ ].freeze,
128
+ cover: %i[
129
+ command_topic
130
+ device_class
131
+ payload_close
132
+ payload_open
133
+ payload_stop
134
+ position_closed
135
+ position_open
136
+ position_template
137
+ position_topic
138
+ set_position_template
139
+ set_position_topic
140
+ state_closed
141
+ state_closing
142
+ state_open
143
+ state_opening
144
+ state_stopped
145
+ state_topic
146
+ tilt_closed_value
147
+ tilt_command_topic
148
+ tilt_max
149
+ tilt_min
150
+ tilt_opened_value
151
+ tilt_optimistic
152
+ tilt_range
153
+ tilt_status_template
154
+ value_template
155
+ ].freeze,
156
+ fan: %i[
157
+ command_topic:
158
+ command_template
159
+ direction_command_template
160
+ direction_command_topic
161
+ direction_state_topic
162
+ direction_value_template
163
+ oscillation_command_template
164
+ oscillation_command_topic
165
+ oscillation_state_topic
166
+ oscillation_value_template
167
+ payload_off
168
+ payload_on
169
+ payload_oscillation_off
170
+ payload_oscillation_on
171
+ payload_reset_percentage
172
+ payload_reset_preset_mode
173
+ percentage_command_template
174
+ percentage_command_topic
175
+ percentage_state_topic
176
+ percentage_value_template
177
+ preset_mode_command_template
178
+ preset_mode_command_topic
179
+ preset_mode_state_topic
180
+ preset_mode_value_template
181
+ preset_modes
182
+ speed_range
183
+ state_topic
184
+ state_value_template
185
+ ].freeze,
186
+ humidifier: %i[
187
+ action_template
188
+ action_topic
189
+ current_humidity_template
190
+ current_humidity_topic
191
+ command_template
192
+ command_topic
193
+ device_class
194
+ mode_command_template
195
+ mode_command_topic
196
+ mode_staet_template
197
+ mode_state_topic
198
+ modes
199
+ payload_off
200
+ payload_on
201
+ payload_reset_humidity
202
+ payload_reset_mode
203
+ state_topic
204
+ target_humidity_command_template
205
+ target_humidity_command_topic
206
+ target_humidity_state_topic
207
+ target_humidity_state_template
208
+ ].freeze,
209
+ light: {
210
+ default: %i[
211
+ brightness_command_template
212
+ brightness_command_topic
213
+ brightness_scale
214
+ brightness_state_topic
215
+ brightness_value_template
216
+ color_mode_state_topic
217
+ color_mode_value_template
218
+ color_temp_command_template
219
+ color_temp_command_topic
220
+ color_temp_state_topic
221
+ color_temp_value_template
222
+ command_topic
223
+ effect_command_topic
224
+ effect_command_template
225
+ effect_list
226
+ effect_state_topic
227
+ effect_value_template
228
+ hs_command_template
229
+ hs_command_topic
230
+ hs_state_topic
231
+ hs_value_template
232
+ max_mireds
233
+ min_mireds
234
+ mireds_range
235
+ on_command_type
236
+ payload_off
237
+ payload_on
238
+ rgb_command_template
239
+ rgb_command_topic
240
+ rgb_state_topic
241
+ rgb_value_template
242
+ rgbw_command_template
243
+ rgbw_command_topic
244
+ rgbw_state_topic
245
+ rgbw_value_template
246
+ rgbww_command_template
247
+ rgbww_command_topic
248
+ rgbww_state_topic
249
+ rgbww_value_template
250
+ state_topic
251
+ white_command_topic
252
+ white_scale
253
+ xy_command_template
254
+ xy_command_topic
255
+ xy_state_topic
256
+ xy_value_template
257
+ ].freeze,
258
+ json: %i[
259
+ brightness
260
+ brightness_scale
261
+ command_topic
262
+ effect
263
+ effect_list
264
+ flash_time_long
265
+ flash_time_short
266
+ max_mireds
267
+ min_mireds
268
+ mireds_range
269
+ state_topic
270
+ supported_color_modes
271
+ white_scale
272
+ ].freeze,
273
+ template: %i[
274
+ blue_template
275
+ brightness_template
276
+ color_temp_template
277
+ command_off_template
278
+ command_on_template
279
+ command_topic
280
+ effect_list
281
+ effect_template
282
+ green_template
283
+ max_mireds
284
+ min_mireds
285
+ mireds_range
286
+ red_template
287
+ state_template
288
+ state_topic
289
+ ].freeze
290
+ }.freeze,
291
+ number: %i[
292
+ command_template
293
+ command_topic
294
+ min
295
+ max
296
+ mode
297
+ payload_reset
298
+ range
299
+ state_topic
300
+ step
301
+ unit_of_measurement
302
+ value_template
303
+ ].freeze,
304
+ scene: %i[
305
+ command_topic
306
+ payload_on
307
+ ].freeze,
308
+ select: %i[
309
+ command_template
310
+ command_topic
311
+ options
312
+ state_topic
313
+ value_template
314
+ ].freeze,
315
+ sensor: %i[
316
+ device_class
317
+ expire_after
318
+ force_update
319
+ last_reset_value_template
320
+ options
321
+ suggested_display_precision
322
+ state_class
323
+ state_topic
324
+ unit_of_measurement
325
+ value_template
326
+ ].freeze,
327
+ switch: %i[
328
+ command_template
329
+ command_topic
330
+ device_class
331
+ payload_off
332
+ payload_on
333
+ state_off
334
+ state_on
335
+ state_topic
336
+ value_template
337
+ ].freeze,
338
+ text: %i[
339
+ command_template
340
+ command_topic
341
+ min
342
+ max
343
+ range
344
+ mode
345
+ pattern
346
+ state_topic
347
+ value_template
348
+ ].freeze,
349
+ water_heater: %i[
350
+ current_temperature_template
351
+ current_temperature_topic
352
+ initial
353
+ max_temp
354
+ min_temp
355
+ mode_command_template
356
+ mode_command_topic
357
+ mode_state_template
358
+ mode_state_topic
359
+ modes
360
+ payload_off
361
+ payload_on
362
+ power_command_template
363
+ power_command_topic
364
+ precision
365
+ range
366
+ temperature_command_template
367
+ temperature_command_topic
368
+ temperature_state_template
369
+ temperature_state_topic
370
+ temperature_unit
371
+ value_template
372
+ ]
373
+ }.freeze
374
+
375
+ RANGE_ATTRIBUTES = {
376
+ climate: { humidity: :prefix, temp: :prefix }.freeze,
377
+ cover: { tilt: :suffix }.freeze,
378
+ fan: { speed_range: :suffix }.freeze,
379
+ humidifier: { humidity: :prefix }.freeze,
380
+ light: { mireds: :prefix }.freeze,
381
+ number: { range: :singleton }.freeze,
382
+ text: { range: :singleton }.freeze,
383
+ water_heater: { range: :singleton }.freeze
384
+ }.freeze
385
+
386
+ REQUIRED_ATTRIBUTES = {
387
+ binary_sensor: %i[state_topic].freeze,
388
+ button: %i[command_topic].freeze,
389
+ humidifier: %i[command_topic target_humidity_command_topic].freeze,
390
+ light: {
391
+ default: %i[command_topic].freeze,
392
+ json: %i[command_topic].freeze,
393
+ template: %i[command_off_template command_on_template command_topic]
394
+ }.freeze,
395
+ number: %i[command_topic].freeze,
396
+ select: %i[command_topic options].freeze,
397
+ sensor: %i[state_topic].freeze,
398
+ switch: %i[command_topic].freeze,
399
+ text: %i[command_topic].freeze
400
+ }.freeze
401
+
402
+ DEFAULTS = {
403
+ binary_sensor: {
404
+ payload_off: "OFF",
405
+ payload_on: "ON"
406
+ }.freeze,
407
+ button: {
408
+ payload_press: "PRESS"
409
+ }.freeze,
410
+ climate: {
411
+ fan_modes: %w[auto low medium high].freeze,
412
+ modes: %w[auto off cool heat dry fan_only].freeze,
413
+ swing_modes: %w[on off].freeze
414
+ }.freeze,
415
+ cover: {
416
+ payload_close: "CLOSE",
417
+ payload_open: "OPEN",
418
+ payload_stop: "STOP",
419
+ state_closed: "closed",
420
+ state_closing: "closing",
421
+ state_open: "open",
422
+ state_opening: "opening",
423
+ state_stopped: "stopped"
424
+ }.freeze,
425
+ fan: {
426
+ payload_off: "off",
427
+ payload_on: "on"
428
+ }.freeze,
429
+ humidifier: {
430
+ device_class: "humidifier",
431
+ payload_off: "OFF",
432
+ payload_on: "ON",
433
+ payload_reset_humidity: "None",
434
+ payload_reset_mode: "None"
435
+ }.freeze,
436
+ light: {
437
+ payload_off: "OFF",
438
+ payload_on: "ON"
439
+ }.freeze,
440
+ number: {
441
+ mode: "auto",
442
+ payload_reset: "None"
443
+ }.freeze,
444
+ scene: {
445
+ payload_on: "ON"
446
+ },
447
+ switch: {
448
+ payload_off: "OFF",
449
+ payload_on: "ON"
450
+ }.freeze,
451
+ text: {
452
+ mode: "text"
453
+ }.freeze,
454
+ water_heater: {
455
+ modes: %i[off eco electric gas heat_pump high_demand performance].freeze,
456
+ payload_off: "OFF",
457
+ payload_on: "ON"
458
+ }.freeze
459
+ }.freeze
460
+
461
+ VALIDATIONS = {
462
+ light: lambda do |supported_color_modes: nil, **|
463
+ if supported_color_modes && supported_color_modes.length > 1 &&
464
+ (supported_color_modes.include?(:onoff) || supported_color_modes.include?(:brightness))
465
+ raise ArgumentError,
466
+ "Multiple color modes are not supported for platform light if onoff or brightness are specified"
467
+ end
468
+ end
469
+ }.freeze
470
+
471
+ SUBSET_VALIDATIONS = {
472
+ climate: {
473
+ modes: DEFAULTS[:climate][:modes]
474
+ }.freeze,
475
+ light: {
476
+ supported_color_modes: %i[onoff brightness color_temp hs xy rgb rgbw rgbww white].freeze
477
+ }.freeze,
478
+ water_heater: {
479
+ modes: DEFAULTS[:water_heater][:modes]
480
+ }
481
+ }.freeze
482
+ INCLUSION_VALIDATIONS = {
483
+ common: {
484
+ entity_category: %i[config diagnostic system].freeze
485
+ }.freeze,
486
+ binary_sensor: {
487
+ device_class: %i[
16
488
  battery
17
489
  battery_charging
18
490
  carbon_monoxide
@@ -41,12 +513,40 @@ module MQTT
41
513
  update
42
514
  vibration
43
515
  window
44
- ].freeze,
45
- humidifier: %i[
516
+ ].to_set.freeze
517
+ }.freeze,
518
+ button: {
519
+ device_class: %i[
520
+ identify
521
+ restart
522
+ update
523
+ ].freeze
524
+ }.freeze,
525
+ cover: {
526
+ device_class: %i[
527
+ awning
528
+ blind
529
+ curtain
530
+ damper
531
+ door
532
+ garage
533
+ gate
534
+ shade
535
+ shutter
536
+ window
537
+ ].freeze
538
+ }.freeze,
539
+ humidifier: {
540
+ device_class: %i[
46
541
  humidifier
47
542
  dehumidifier
48
- ].freeze,
49
- sensor: %i[
543
+ ].freeze
544
+ }.freeze,
545
+ light: {
546
+ on_command_type: %i[last first brightness].freeze
547
+ }.freeze,
548
+ sensor: {
549
+ device_class: %i[
50
550
  apparent_power
51
551
  aqi
52
552
  atmospheric_pressure
@@ -97,479 +597,14 @@ module MQTT
97
597
  water
98
598
  weight
99
599
  wind_speed
100
- ].freeze
101
- }.freeze
102
- STATE_CLASSES = %i[measurement total total_increasing].freeze
103
- ON_COMMAND_TYPES = %i[last first brightness].freeze
104
-
105
- # @param property [MQTT::Homie::Property] A Homie property object of datatype :boolean
106
- def publish_binary_sensor(
107
- property,
108
- device_class: nil,
109
- expire_after: nil,
110
- force_update: false,
111
- off_delay: nil,
112
-
113
- device: nil,
114
- discovery_prefix: nil,
115
- entity_category: nil,
116
- icon: nil
117
- )
118
- raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
119
- if device_class && !DEVICE_CLASSES[:binary_sensor].include?(device_class)
120
- raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
121
- end
122
-
123
- config = base_config(property.device,
124
- "#{property.node.name} #{property.name}",
125
- device_class: device_class,
126
- device: device,
127
- entity_category: entity_category,
128
- icon: icon)
129
- .merge({
130
- payload_off: "false",
131
- payload_on: "true",
132
- object_id: "#{property.node.id}_#{property.id}",
133
- state_topic: property.topic
134
- })
135
- config[:expire_after] = expire_after if expire_after
136
- config[:force_update] = true if force_update
137
- config[:off_delay] = off_delay if off_delay
138
-
139
- publish(property.mqtt, "binary_sensor", config, discovery_prefix: discovery_prefix)
140
- end
141
-
142
- def publish_climate(
143
- action_property: nil,
144
- aux_property: nil,
145
- away_mode_property: nil,
146
- current_temperature_property: nil,
147
- fan_mode_property: nil,
148
- mode_property: nil,
149
- hold_property: nil,
150
- power_property: nil,
151
- swing_mode_property: nil,
152
- temperature_property: nil,
153
- temperature_high_property: nil,
154
- temperature_low_property: nil,
155
- name: nil,
156
- id: nil,
157
- precision: nil,
158
- temp_step: nil,
159
-
160
- device: nil,
161
- discovery_prefix: nil,
162
- entity_category: nil,
163
- icon: nil,
164
- templates: {}
165
- )
166
- properties = {
167
- action: action_property,
168
- aux: aux_property,
169
- away_mode: away_mode_property,
170
- current_temperature: current_temperature_property,
171
- fan_mode: fan_mode_property,
172
- mode: mode_property,
173
- hold: hold_property,
174
- power: power_property,
175
- swing_mode: swing_mode_property,
176
- temperature: temperature_property,
177
- temperature_high: temperature_high_property,
178
- temperature_low: temperature_low_property
179
- }.compact
180
- raise ArgumentError, "At least one property must be specified" if properties.empty?
181
- raise ArgumentError, "Power property must be a boolean" if power_property && power_property.datatype != :boolean
182
-
183
- node = properties.first.last.node
184
-
185
- config = base_config(node.device,
186
- name || node.name,
187
- device: device,
188
- entity_category: entity_category,
189
- icon: icon)
190
-
191
- config[:object_id] = id || node.id
192
- read_only_props = %i[action current_temperature]
193
- properties.each do |prefix, property|
194
- add_property(config, property, prefix, templates: templates, read_only: read_only_props.include?(prefix))
195
- end
196
- temp_properties = [
197
- temperature_property,
198
- temperature_high_property,
199
- temperature_low_property
200
- ].compact
201
- unless (temp_ranges = temp_properties.map(&:range).compact).empty?
202
- config[:min_temp] = temp_ranges.map(&:begin).min
203
- config[:max_temp] = temp_ranges.map(&:end).max
204
- end
205
- temperature_unit = temp_properties.map(&:unit).compact.first
206
- config[:temperature_unit] = temperature_unit[-1] if temperature_unit
207
- {
208
- nil => mode_property,
209
- :fan => fan_mode_property,
210
- :hold => hold_property,
211
- :swing => swing_mode_property
212
- }.compact.each do |prefix, property|
213
- valid_set = %w[auto off cool heat dry fan_only] if prefix.nil?
214
- add_enum(config, property, prefix, valid_set)
215
- end
216
- config[:precision] = precision if precision
217
- config[:temp_step] = temp_step if temp_step
218
- if power_property
219
- config[:payload_on] = "true"
220
- config[:payload_off] = "false"
221
- end
222
-
223
- publish(node.mqtt, "climate", config, discovery_prefix: discovery_prefix)
224
- end
225
-
226
- def publish_fan(
227
- property,
228
- oscillation_property: nil,
229
- percentage_property: nil,
230
- preset_mode_property: nil,
231
-
232
- device: nil,
233
- discovery_prefix: nil,
234
- entity_category: nil,
235
- icon: nil
236
- )
237
- config = base_config(property.device,
238
- name || property.node.name,
239
- device: device,
240
- device_class: device_class,
241
- entity_category: entity_category,
242
- icon: icon,
243
- templates: {})
244
- add_property(config, oscillation_property, :oscillation_property, templates: templates)
245
- add_property(config, percentage_property, :percentage, templates: templates)
246
- if percentage_property&.range
247
- config[:speed_range_min] = percentage_property.range.begin
248
- config[:speed_range_max] = percentage_property.range.end
249
- end
250
- add_property(config, preset_mode_property, :preset, templates: templates)
251
- add_enum(config, preset_mode_property, :preset)
252
-
253
- publish(node.mqtt, "fan", config, discovery_prefix: discovery_prefix)
254
- end
255
-
256
- def publish_humidifier(
257
- property,
258
- device_class:,
259
- target_property:,
260
- mode_property: nil,
261
- name: nil,
262
- id: nil,
263
-
264
- device: nil,
265
- discovery_prefix: nil,
266
- entity_category: nil,
267
- icon: nil
268
- )
269
- raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
270
-
271
- unless DEVICE_CLASSES[:humidifier].include?(device_class)
272
- raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
273
- end
274
-
275
- config = base_config(property.device,
276
- name || property.node.name,
277
- device: device,
278
- device_class: device_class,
279
- entity_category: entity_category,
280
- icon: icon)
281
- .merge({
282
- command_topic: "#{property.topic}/set",
283
- target_humidity_command_topic: "#{target_property.topic}/set",
284
- payload_off: "false",
285
- payload_on: "true",
286
- object_id: id || property.node.id
287
- })
288
- add_property(config, property)
289
- add_property(config, target_property, :target_humidity)
290
- if (range = target_property.range)
291
- config[:min_humidity] = range.begin
292
- config[:max_humidity] = range.end
293
- end
294
- add_property(config, mode_property, :mode)
295
- add_enum(config, mode_property)
296
-
297
- publish(property.mqtt, "humidifier", config, discovery_prefix: discovery_prefix)
298
- end
299
-
300
- # `default` schema only for now
301
- def publish_light(
302
- property = nil,
303
- brightness_property: nil,
304
- color_mode_property: nil,
305
- color_temp_property: nil,
306
- effect_property: nil,
307
- hs_property: nil,
308
- rgb_property: nil,
309
- white_property: nil,
310
- xy_property: nil,
311
- on_command_type: nil,
312
-
313
- device: nil,
314
- discovery_prefix: nil,
315
- entity_category: nil,
316
- icon: nil,
317
- templates: {}
318
- )
319
- if on_command_type && !ON_COMMAND_TYPES.include?(on_command_type)
320
- raise ArgumentError, "Invalid on_command_type #{on_command_type.inspect}"
321
- end
322
-
323
- # automatically infer a brightness-only light and adjust config
324
- if brightness_property && property.nil?
325
- property = brightness_property
326
- on_command_type = :brightness
327
- end
328
-
329
- config = base_config(property.device,
330
- "#{property.node.name} #{property.name}",
331
- device: device,
332
- entity_category: entity_category,
333
- icon: icon)
334
- config[:object_id] = "#{property.node.id}_#{property.id}"
335
- add_property(config, property)
336
- case property.datatype
337
- when :boolean
338
- config[:payload_off] = "false"
339
- config[:payload_on] = "true"
340
- when :integer
341
- config[:payload_off] = "0"
342
- when :float
343
- config[:payload_off] = "0.0"
344
- end
345
- add_property(config, brightness_property, :brightness, templates: templates)
346
- config[:brightness_scale] = brightness_property.range.end if brightness_property&.range
347
- add_property(config, color_mode_property, :color_mode, templates: templates)
348
- add_property(config, color_temp_property, :color_temp, templates: templates)
349
- if color_temp_property&.range && color_temp_property.unit == "mired"
350
- config[:min_mireds] = color_temp_property.range.begin
351
- config[:max_mireds] = color_temp_property.range.end
352
- end
353
- add_property(config, effect_property, :effect, templates: templates)
354
- config[:effect_list] = effect_property.range if effect_property&.datatype == :enum
355
- add_property(config, hs_property, :hs, templates: templates)
356
- add_property(config, rgb_property, :rgb, templates: templates)
357
- add_property(config, white_property, :white, templates: templates)
358
- config[:white_scale] = white_property.range.end if white_property&.range
359
- add_property(config, xy_property, :xy, templates: templates)
360
- config[:on_command_type] = on_command_type if on_command_type
361
-
362
- publish(property.mqtt, "light", config, discovery_prefix: discovery_prefix)
363
- end
364
-
365
- def publish_number(
366
- property,
367
- step: nil,
368
-
369
- device: nil,
370
- discovery_prefix: nil,
371
- entity_category: nil,
372
- icon: nil
373
- )
374
- raise ArgumentError, "Homie property must be an integer or a float" unless %i[integer
375
- float].include?(property.datatype)
376
-
377
- config = base_config(property.device,
378
- "#{property.node.name} #{property.name}",
379
- device: device,
380
- entity_category: entity_category,
381
- icon: icon)
382
- config[:object_id] = "#{property.node.id}_#{property.id}"
383
- add_property(config, property)
384
- config[:unit_of_measurement] = property.unit if property.unit
385
- if property.range
386
- config[:min] = property.range.begin
387
- config[:max] = property.range.end
388
- end
389
- config[:step] = step if step
390
-
391
- publish(property.mqtt, "number", config, discovery_prefix: discovery_prefix)
392
- end
393
-
394
- def publish_scene(
395
- property,
396
-
397
- device: nil,
398
- discovery_prefix: nil,
399
- entity_category: nil,
400
- icon: nil
401
- )
402
- unless property.datatype == :enum && property.range.length == 1
403
- raise ArgumentError, "Homie property must be an enum with a single value"
404
- end
405
-
406
- config = base_config(property.device,
407
- "#{property.node.name} #{property.name}",
408
- device: device,
409
- entity_category: entity_category,
410
- icon: icon)
411
- config[:object_id] = "#{property.node.id}_#{property.id}"
412
- add_property(config, property)
413
- config[:payload_on] = property.range.first
414
-
415
- publish(property.mqtt, "scene", config, discovery_prefix: discovery_prefix)
416
- end
417
-
418
- def publish_select(
419
- property,
420
-
421
- device: nil,
422
- discovery_prefix: nil,
423
- entity_category: nil,
424
- icon: nil
425
- )
426
- raise ArgumentError, "Homie property must be an enum" unless property.datatype == :enum
427
- raise ArgumentError, "Homie property must be settable" unless property.settable?
428
-
429
- config = base_config(property.device,
430
- "#{property.node.name} #{property.name}",
431
- device: device,
432
- entity_category: entity_category,
433
- icon: icon)
434
- config[:object_id] = "#{property.node.id}_#{property.id}"
435
- add_property(config, property)
436
- config[:options] = property.range
437
-
438
- publish(property.mqtt, "select", config, discovery_prefix: discovery_prefix)
439
- end
440
-
441
- # @param property [MQTT::Homie::Property] A Homie property object
442
- def publish_sensor(
443
- property,
444
- device_class: nil,
445
- expire_after: nil,
446
- force_update: false,
447
- state_class: nil,
448
-
449
- device: nil,
450
- discovery_prefix: nil,
451
- entity_category: nil,
452
- icon: nil
453
- )
454
- device_class ||= :enum if property.datatype == :enum
455
- if device_class && !DEVICE_CLASSES[:sensor].include?(device_class)
456
- raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
457
- end
458
- if state_class && !STATE_CLASSES.include?(state_class)
459
- raise ArgumentError, "Unrecognized state_class #{state_class.inspect}"
460
- end
461
-
462
- config = base_config(property.device,
463
- "#{property.node.name} #{property.name}",
464
- device: device,
465
- device_class: device_class,
466
- entity_category: entity_category,
467
- icon: icon)
468
- .merge({
469
- object_id: "#{property.node.id}_#{property.id}",
470
- state_topic: property.topic
471
- })
472
- config[:state_class] = state_class if state_class
473
- config[:expire_after] = expire_after if expire_after
474
- config[:force_update] = true if force_update
475
- config[:unit_of_measurement] = property.unit if property.unit
476
-
477
- publish(property.mqtt, "sensor", config, discovery_prefix: discovery_prefix)
478
- end
479
-
480
- # @param property [MQTT::Homie::Property] A Homie property object of datatype :boolean
481
- def publish_switch(property,
482
- device_class: nil,
483
-
484
- device: nil,
485
- discovery_prefix: nil,
486
- entity_category: nil,
487
- icon: nil)
488
- raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
489
-
490
- config = base_config(property.device,
491
- "#{property.node.name} #{property.name}",
492
- device: device,
493
- device_class: device_class,
494
- entity_category: entity_category,
495
- icon: icon)
496
- .merge({
497
- object_id: "#{property.node.id}_#{property.id}",
498
- payload_off: "false",
499
- payload_on: "true"
500
- })
501
- add_property(config, property)
502
-
503
- publish(property.mqtt, "switch", config, discovery_prefix: discovery_prefix)
504
- end
505
-
506
- private
507
-
508
- def add_property(config, property, prefix = nil, templates: {}, read_only: false)
509
- return unless property
510
-
511
- prefix = "#{prefix}_" if prefix
512
- state_prefix = "state_" unless read_only
513
- config[:"#{prefix}#{state_prefix}topic"] = property.topic if property.retained?
514
- if !read_only && property.settable?
515
- config[:"#{prefix}command_topic"] = "#{property.topic}/set"
516
- config[:"#{prefix}command_template"] = "{{ value | round(0) }}" if property.datatype == :integer
517
- end
518
- config.merge!(templates.slice(:"#{prefix}template", :"#{prefix}command_template"))
519
- end
520
-
521
- def add_enum(config, property, prefix = nil, valid_set = nil)
522
- prefix = "#{prefix}_" if prefix
523
-
524
- return unless property&.datatype == :enum
525
-
526
- modes = property.range
527
- modes &= valid_set if valid_set
528
- config[:"#{prefix}modes"] = modes
529
- end
530
-
531
- def base_config(homie_device,
532
- name,
533
- device:,
534
- entity_category:,
535
- icon:,
536
- device_class: nil)
537
- if entity_category && !ENTITY_CATEGORIES.include?(entity_category)
538
- raise ArgumentError, "Unrecognized entity_category #{entity_category.inspect}"
539
- end
540
-
541
- config = {
542
- name: name,
543
- node_id: homie_device.id,
544
- availability_topic: "#{homie_device.topic}/$state",
545
- payload_available: "ready",
546
- payload_not_available: "lost",
547
- qos: 1
548
- }
549
- config[:device_class] = device_class if device_class
550
- config[:entity_category] = entity_category if entity_category
551
- config[:icon] = icon if icon
552
-
553
- device = device&.dup || {}
554
- device[:name] ||= homie_device.name
555
- device[:sw_version] ||= MQTT::Homie::Device::VERSION
556
- device[:identifiers] ||= homie_device.id unless device[:connections]
557
- config[:device] = device
558
-
559
- config
560
- end
561
-
562
- def publish(mqtt, component, config, discovery_prefix:)
563
- node_id, object_id = config.values_at(:node_id, :object_id)
564
- config = config.dup
565
- config[:unique_id] = "#{node_id}_#{object_id}"
566
- config.delete(:node_id)
567
- config.delete(:object_id)
568
- mqtt.publish("#{discovery_prefix || "homeassistant"}/#{component}/#{node_id}/#{object_id}/config",
569
- config.to_json,
570
- retain: true,
571
- qos: 1)
572
- end
573
- end
600
+ ].to_set.freeze,
601
+ state_class: %i[measurement total total_increasing].freeze
602
+ }.freeze,
603
+ text: {
604
+ mode: %i[text password].freeze
605
+ }
606
+ }.freeze
574
607
  end
575
608
  end
609
+
610
+ require "mqtt/home_assistant/client"