mqtt-homeassistant 0.1.6 → 1.0.0

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