mqtt-homeassistant 0.1.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"