mqtt-homeassistant 0.1.5 → 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,19 +2,492 @@
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
490
+ carbon_monoxide
18
491
  cold
19
492
  connectivity
20
493
  door
@@ -40,511 +513,98 @@ module MQTT
40
513
  update
41
514
  vibration
42
515
  window
43
- ].freeze,
44
- 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[
45
541
  humidifier
46
542
  dehumidifier
47
- ].freeze,
48
- 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[
550
+ apparent_power
49
551
  aqi
552
+ atmospheric_pressure
50
553
  battery
51
554
  carbon_dioxide
52
555
  carbon_monoxide
53
556
  current
557
+ data_rate
558
+ data_size
54
559
  date
560
+ distance
561
+ duration
55
562
  energy
563
+ energy_storage
564
+ enum
565
+ frequency
56
566
  gas
57
567
  humidity
58
568
  illuminance
569
+ irradiance
570
+ moisture
59
571
  monetary
60
572
  nitrogen_dioxide
61
573
  nitrogen_monoxide
62
574
  nitrous_oxide
63
575
  ozone
576
+ ph
64
577
  pm1
65
578
  pm10
66
579
  pm25
67
580
  power_factor
68
581
  power
582
+ precipitation
583
+ precipitation_intensity
69
584
  pressure
585
+ reactive_power
70
586
  signal_strength
587
+ sound_pressure
588
+ speed
71
589
  sulphur_dioxide
72
590
  temperature
73
591
  timestamp
74
592
  volatile_organic_compounds
593
+ volatile_organic_compounds_parts
75
594
  voltage
76
- ].freeze
77
- }.freeze
78
- STATE_CLASSES = %i[measurement total total_increasing].freeze
79
- ON_COMMAND_TYPES = %i[last first brightness].freeze
80
-
81
- # @param property [MQTT::Homie::Property] A Homie property object of datatype :boolean
82
- def publish_binary_sensor(
83
- property,
84
- device_class: nil,
85
- expire_after: nil,
86
- force_update: false,
87
- off_delay: nil,
88
-
89
- device: nil,
90
- discovery_prefix: nil,
91
- entity_category: nil,
92
- icon: nil
93
- )
94
- raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
95
- if device_class && !DEVICE_CLASSES[:binary_sensor].include?(device_class)
96
- raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
97
- end
98
-
99
- config = base_config(property.device,
100
- "#{property.node.name} #{property.name}",
101
- device_class: device_class,
102
- device: device,
103
- entity_category: entity_category,
104
- icon: icon)
105
- .merge({
106
- payload_off: "false",
107
- payload_on: "true",
108
- object_id: "#{property.node.id}_#{property.id}",
109
- state_topic: property.topic
110
- })
111
- config[:expire_after] = expire_after if expire_after
112
- config[:force_update] = true if force_update
113
- config[:off_delay] = off_delay if off_delay
114
-
115
- publish(property.mqtt, "binary_sensor", config, discovery_prefix: discovery_prefix)
116
- end
117
-
118
- def publish_climate(
119
- action_property: nil,
120
- aux_property: nil,
121
- away_mode_property: nil,
122
- current_temperature_property: nil,
123
- fan_mode_property: nil,
124
- mode_property: nil,
125
- hold_property: nil,
126
- power_property: nil,
127
- swing_mode_property: nil,
128
- temperature_property: nil,
129
- temperature_high_property: nil,
130
- temperature_low_property: nil,
131
- name: nil,
132
- id: nil,
133
- precision: nil,
134
- temp_step: nil,
135
-
136
- device: nil,
137
- discovery_prefix: nil,
138
- entity_category: nil,
139
- icon: nil,
140
- templates: {}
141
- )
142
- properties = {
143
- action: action_property,
144
- aux: aux_property,
145
- away_mode: away_mode_property,
146
- current_temperature: current_temperature_property,
147
- fan_mode: fan_mode_property,
148
- mode: mode_property,
149
- hold: hold_property,
150
- power: power_property,
151
- swing_mode: swing_mode_property,
152
- temperature: temperature_property,
153
- temperature_high: temperature_high_property,
154
- temperature_low: temperature_low_property
155
- }.compact
156
- raise ArgumentError, "At least one property must be specified" if properties.empty?
157
- raise ArgumentError, "Power property must be a boolean" if power_property && power_property.datatype != :boolean
158
-
159
- node = properties.first.last.node
160
-
161
- config = base_config(node.device,
162
- name || node.name,
163
- device: device,
164
- entity_category: entity_category,
165
- icon: icon)
166
-
167
- config[:object_id] = id || node.id
168
- read_only_props = %i[action current_temperature]
169
- properties.each do |prefix, property|
170
- add_property(config, property, prefix, templates: templates, read_only: read_only_props.include?(prefix))
171
- end
172
- temp_properties = [
173
- temperature_property,
174
- temperature_high_property,
175
- temperature_low_property
176
- ].compact
177
- unless (temp_ranges = temp_properties.map(&:range).compact).empty?
178
- config[:min_temp] = temp_ranges.map(&:begin).min
179
- config[:max_temp] = temp_ranges.map(&:end).max
180
- end
181
- temperature_unit = temp_properties.map(&:unit).compact.first
182
- config[:temperature_unit] = temperature_unit[-1] if temperature_unit
183
- {
184
- nil => mode_property,
185
- :fan => fan_mode_property,
186
- :hold => hold_property,
187
- :swing => swing_mode_property
188
- }.compact.each do |prefix, property|
189
- valid_set = %w[auto off cool heat dry fan_only] if prefix.nil?
190
- add_enum(config, property, prefix, valid_set)
191
- end
192
- config[:precision] = precision if precision
193
- config[:temp_step] = temp_step if temp_step
194
- if power_property
195
- config[:payload_on] = "true"
196
- config[:payload_off] = "false"
197
- end
198
-
199
- publish(node.mqtt, "climate", config, discovery_prefix: discovery_prefix)
200
- end
201
-
202
- def publish_fan(
203
- property,
204
- oscillation_property: nil,
205
- percentage_property: nil,
206
- preset_mode_property: nil,
207
-
208
- device: nil,
209
- discovery_prefix: nil,
210
- entity_category: nil,
211
- icon: nil
212
- )
213
- config = base_config(property.device,
214
- name || property.node.name,
215
- device: device,
216
- device_class: device_class,
217
- entity_category: entity_category,
218
- icon: icon,
219
- templates: {})
220
- add_property(config, oscillation_property, :oscillation_property, templates: templates)
221
- add_property(config, percentage_property, :percentage, templates: templates)
222
- if percentage_property&.range
223
- config[:speed_range_min] = percentage_property.range.begin
224
- config[:speed_range_max] = percentage_property.range.end
225
- end
226
- add_property(config, preset_mode_property, :preset, templates: templates)
227
- add_enum(config, preset_mode_property, :preset)
228
-
229
- publish(node.mqtt, "fan", config, discovery_prefix: discovery_prefix)
230
- end
231
-
232
- def publish_humidifier(
233
- property,
234
- device_class:,
235
- target_property:,
236
- mode_property: nil,
237
- name: nil,
238
- id: nil,
239
-
240
- device: nil,
241
- discovery_prefix: nil,
242
- entity_category: nil,
243
- icon: nil
244
- )
245
- raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
246
-
247
- unless DEVICE_CLASSES[:humidifier].include?(device_class)
248
- raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
249
- end
250
-
251
- config = base_config(property.device,
252
- name || property.node.name,
253
- device: device,
254
- device_class: device_class,
255
- entity_category: entity_category,
256
- icon: icon)
257
- .merge({
258
- command_topic: "#{property.topic}/set",
259
- target_humidity_command_topic: "#{target_property.topic}/set",
260
- payload_off: "false",
261
- payload_on: "true",
262
- object_id: id || property.node.id
263
- })
264
- add_property(config, property)
265
- add_property(config, target_property, :target_humidity)
266
- if (range = target_property.range)
267
- config[:min_humidity] = range.begin
268
- config[:max_humidity] = range.end
269
- end
270
- add_property(config, mode_property, :mode)
271
- add_enum(config, mode_property)
272
-
273
- publish(property.mqtt, "humidifier", config, discovery_prefix: discovery_prefix)
274
- end
275
-
276
- # `default` schema only for now
277
- def publish_light(
278
- property = nil,
279
- brightness_property: nil,
280
- color_mode_property: nil,
281
- color_temp_property: nil,
282
- effect_property: nil,
283
- hs_property: nil,
284
- rgb_property: nil,
285
- white_property: nil,
286
- xy_property: nil,
287
- on_command_type: nil,
288
-
289
- device: nil,
290
- discovery_prefix: nil,
291
- entity_category: nil,
292
- icon: nil,
293
- templates: {}
294
- )
295
- if on_command_type && !ON_COMMAND_TYPES.include?(on_command_type)
296
- raise ArgumentError, "Invalid on_command_type #{on_command_type.inspect}"
297
- end
298
-
299
- # automatically infer a brightness-only light and adjust config
300
- if brightness_property && property.nil?
301
- property = brightness_property
302
- on_command_type = :brightness
303
- end
304
-
305
- config = base_config(property.device,
306
- "#{property.node.name} #{property.name}",
307
- device: device,
308
- entity_category: entity_category,
309
- icon: icon)
310
- config[:object_id] = "#{property.node.id}_#{property.id}"
311
- add_property(config, property)
312
- case property.datatype
313
- when :boolean
314
- config[:payload_off] = "false"
315
- config[:payload_on] = "true"
316
- when :integer
317
- config[:payload_off] = "0"
318
- when :float
319
- config[:payload_off] = "0.0"
320
- end
321
- add_property(config, brightness_property, :brightness, templates: templates)
322
- config[:brightness_scale] = brightness_property.range.end if brightness_property&.range
323
- add_property(config, color_mode_property, :color_mode, templates: templates)
324
- add_property(config, color_temp_property, :color_temp, templates: templates)
325
- if color_temp_property&.range && color_temp_property.unit == "mired"
326
- config[:min_mireds] = color_temp_property.range.begin
327
- config[:max_mireds] = color_temp_property.range.end
328
- end
329
- add_property(config, effect_property, :effect, templates: templates)
330
- config[:effect_list] = effect_property.range if effect_property&.datatype == :enum
331
- add_property(config, hs_property, :hs, templates: templates)
332
- add_property(config, rgb_property, :rgb, templates: templates)
333
- add_property(config, white_property, :white, templates: templates)
334
- config[:white_scale] = white_property.range.end if white_property&.range
335
- add_property(config, xy_property, :xy, templates: templates)
336
- config[:on_command_type] = on_command_type if on_command_type
337
-
338
- publish(property.mqtt, "light", config, discovery_prefix: discovery_prefix)
339
- end
340
-
341
- def publish_number(
342
- property,
343
- step: nil,
344
-
345
- device: nil,
346
- discovery_prefix: nil,
347
- entity_category: nil,
348
- icon: nil
349
- )
350
- raise ArgumentError, "Homie property must be an integer or a float" unless %i[integer
351
- float].include?(property.datatype)
352
-
353
- config = base_config(property.device,
354
- "#{property.node.name} #{property.name}",
355
- device: device,
356
- entity_category: entity_category,
357
- icon: icon)
358
- config[:object_id] = "#{property.node.id}_#{property.id}"
359
- add_property(config, property)
360
- config[:unit_of_measurement] = property.unit if property.unit
361
- if property.range
362
- config[:min] = property.range.begin
363
- config[:max] = property.range.end
364
- end
365
- config[:step] = step if step
366
-
367
- publish(property.mqtt, "number", config, discovery_prefix: discovery_prefix)
368
- end
369
-
370
- def publish_scene(
371
- property,
372
-
373
- device: nil,
374
- discovery_prefix: nil,
375
- entity_category: nil,
376
- icon: nil
377
- )
378
- unless property.datatype == :enum && property.range.length == 1
379
- raise ArgumentError, "Homie property must be an enum with a single value"
380
- end
381
-
382
- config = base_config(property.device,
383
- "#{property.node.name} #{property.name}",
384
- device: device,
385
- entity_category: entity_category,
386
- icon: icon)
387
- config[:object_id] = "#{property.node.id}_#{property.id}"
388
- add_property(config, property)
389
- config[:payload_on] = property.range.first
390
-
391
- publish(property.mqtt, "scene", config, discovery_prefix: discovery_prefix)
392
- end
393
-
394
- def publish_select(
395
- property,
396
-
397
- device: nil,
398
- discovery_prefix: nil,
399
- entity_category: nil,
400
- icon: nil
401
- )
402
- raise ArgumentError, "Homie property must be an enum" unless property.datatype == :enum
403
- raise ArgumentError, "Homie property must be settable" unless property.settable?
404
-
405
- config = base_config(property.device,
406
- "#{property.node.name} #{property.name}",
407
- device: device,
408
- entity_category: entity_category,
409
- icon: icon)
410
- config[:object_id] = "#{property.node.id}_#{property.id}"
411
- add_property(config, property)
412
- config[:options] = property.range
413
-
414
- publish(property.mqtt, "select", config, discovery_prefix: discovery_prefix)
415
- end
416
-
417
- # @param property [MQTT::Homie::Property] A Homie property object
418
- def publish_sensor(
419
- property,
420
- device_class: nil,
421
- expire_after: nil,
422
- force_update: false,
423
- state_class: nil,
424
-
425
- device: nil,
426
- discovery_prefix: nil,
427
- entity_category: nil,
428
- icon: nil
429
- )
430
- if device_class && !DEVICE_CLASSES[:sensor].include?(device_class)
431
- raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
432
- end
433
- if state_class && !STATE_CLASSES.include?(state_class)
434
- raise ArgumentError, "Unrecognized state_class #{state_class.inspect}"
435
- end
436
-
437
- config = base_config(property.device,
438
- "#{property.node.name} #{property.name}",
439
- device: device,
440
- device_class: device_class,
441
- entity_category: entity_category,
442
- icon: icon)
443
- .merge({
444
- object_id: "#{property.node.id}_#{property.id}",
445
- state_topic: property.topic
446
- })
447
- config[:state_class] = state_class if state_class
448
- config[:expire_after] = expire_after if expire_after
449
- config[:force_update] = true if force_update
450
- config[:unit_of_measurement] = property.unit if property.unit
451
-
452
- publish(property.mqtt, "sensor", config, discovery_prefix: discovery_prefix)
453
- end
454
-
455
- # @param property [MQTT::Homie::Property] A Homie property object of datatype :boolean
456
- def publish_switch(property,
457
- device_class: nil,
458
-
459
- device: nil,
460
- discovery_prefix: nil,
461
- entity_category: nil,
462
- icon: nil)
463
- raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
464
-
465
- config = base_config(property.device,
466
- "#{property.node.name} #{property.name}",
467
- device: device,
468
- device_class: device_class,
469
- entity_category: entity_category,
470
- icon: icon)
471
- .merge({
472
- object_id: "#{property.node.id}_#{property.id}",
473
- payload_off: "false",
474
- payload_on: "true"
475
- })
476
- add_property(config, property)
477
-
478
- publish(property.mqtt, "switch", config, discovery_prefix: discovery_prefix)
479
- end
480
-
481
- private
482
-
483
- def add_property(config, property, prefix = nil, templates: {}, read_only: false)
484
- return unless property
485
-
486
- prefix = "#{prefix}_" if prefix
487
- state_prefix = "state_" unless read_only
488
- config[:"#{prefix}#{state_prefix}topic"] = property.topic if property.retained?
489
- if !read_only && property.settable?
490
- config[:"#{prefix}command_topic"] = "#{property.topic}/set"
491
- config[:"#{prefix}command_template"] = "{{ value | round(0) }}" if property.datatype == :integer
492
- end
493
- config.merge!(templates.slice(:"#{prefix}template", :"#{prefix}command_template"))
494
- end
495
-
496
- def add_enum(config, property, prefix = nil, valid_set = nil)
497
- prefix = "#{prefix}_" if prefix
498
-
499
- return unless property&.datatype == :enum
500
-
501
- modes = property.range
502
- modes &= valid_set if valid_set
503
- config[:"#{prefix}modes"] = modes
504
- end
505
-
506
- def base_config(homie_device,
507
- name,
508
- device:,
509
- entity_category:,
510
- icon:,
511
- device_class: nil)
512
- if entity_category && !ENTITY_CATEGORIES.include?(entity_category)
513
- raise ArgumentError, "Unrecognized entity_category #{entity_category.inspect}"
514
- end
515
-
516
- config = {
517
- name: name,
518
- node_id: homie_device.id,
519
- availability_topic: "#{homie_device.topic}/$state",
520
- payload_available: "ready",
521
- payload_not_available: "lost",
522
- qos: 1
523
- }
524
- config[:device_class] = device_class if device_class
525
- config[:entity_category] = entity_category if entity_category
526
- config[:icon] = icon if icon
527
-
528
- device = device&.dup || {}
529
- device[:name] ||= homie_device.name
530
- device[:sw_version] ||= MQTT::Homie::Device::VERSION
531
- device[:identifiers] ||= homie_device.id unless device[:connections]
532
- config[:device] = device
533
-
534
- config
535
- end
536
-
537
- def publish(mqtt, component, config, discovery_prefix:)
538
- node_id, object_id = config.values_at(:node_id, :object_id)
539
- config = config.dup
540
- config[:unique_id] = "#{node_id}_#{object_id}"
541
- config.delete(:node_id)
542
- config.delete(:object_id)
543
- mqtt.publish("#{discovery_prefix || "homeassistant"}/#{component}/#{node_id}/#{object_id}/config",
544
- config.to_json,
545
- retain: true,
546
- qos: 1)
547
- end
548
- end
595
+ volume
596
+ volume_storage
597
+ water
598
+ weight
599
+ wind_speed
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
549
607
  end
550
608
  end
609
+
610
+ require "mqtt/home_assistant/client"