mqtt-homeassistant 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 63f377ed2f680df05fcdd443d7827be88ad49630b6a2e046a1291f3ccbe1fe41
4
+ data.tar.gz: 9f48fd3a21b57073ae31bc488c5f8fda3369a2e3d2ef37d41426e1865d1d307a
5
+ SHA512:
6
+ metadata.gz: e378a1edd2a12093be108cb9b1685393422274fcc65b578dee2e73c0a429a06dc35f17889f113998ac03a3f4a936fb01028a07f201061dc5420b3557b204b0f2
7
+ data.tar.gz: 82f86cf62752b56a357844149c2b51952a9fa448783a29e2396053a77648d73acae97921db149a7745a423b97edbd606bd53185f8431b865026383d7cb7d2227
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module HomeAssistant
5
+ VERSION = "0.0.1"
6
+ end
7
+ end
@@ -0,0 +1,535 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module MQTT
6
+ module HomeAssistant
7
+ class << self
8
+ ENTITY_CATEGORIES = %i[config diagnostic system].freeze
9
+ DEVICE_CLASSES = {
10
+ binary_sensor: %i[
11
+ battery
12
+ battery_charging
13
+ cold
14
+ connectivity
15
+ door
16
+ garage_door
17
+ gas
18
+ heat
19
+ light
20
+ lock
21
+ moisture
22
+ motion
23
+ moving
24
+ occupancy
25
+ opening
26
+ plug
27
+ power
28
+ presence
29
+ problem
30
+ running
31
+ safety
32
+ smoke
33
+ sound
34
+ tamper
35
+ update
36
+ vibration
37
+ window
38
+ ].freeze,
39
+ humidifier: %i[
40
+ humidifier
41
+ dehumidifier
42
+ ].freeze,
43
+ sensor: %i[
44
+ aqi
45
+ battery
46
+ carbon_dioxide
47
+ carbon_monoxide
48
+ current
49
+ date
50
+ energy
51
+ gas
52
+ humidity
53
+ illuminance
54
+ monetary
55
+ nitrogen_dioxide
56
+ nitrogen_monoxide
57
+ nitrous_oxide
58
+ ozone
59
+ pm1
60
+ pm10
61
+ pm25
62
+ power_factor
63
+ power
64
+ pressure
65
+ signal_strength
66
+ sulphur_dioxide
67
+ temperature
68
+ timestamp
69
+ volatile_organic_compounds
70
+ voltage
71
+ ].freeze
72
+ }.freeze
73
+ STATE_CLASSES = %i[measurement total total_increasing].freeze
74
+ ON_COMMAND_TYPES = %i[last first brightness].freeze
75
+
76
+ # @param property [MQTT::Homie::Property] A Homie property object of datatype :boolean
77
+ def publish_binary_sensor(
78
+ property,
79
+ device_class: nil,
80
+ expire_after: nil,
81
+ force_update: false,
82
+ off_delay: nil,
83
+
84
+ device: nil,
85
+ discovery_prefix: nil,
86
+ entity_category: nil,
87
+ icon: nil
88
+ )
89
+ raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
90
+ if device_class && !DEVICE_CLASSES[:binary_sensor].include?(device_class)
91
+ raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
92
+ end
93
+
94
+ config = base_config(property.device,
95
+ property.full_name,
96
+ device_class: device_class,
97
+ device: device,
98
+ entity_category: entity_category,
99
+ icon: icon)
100
+ .merge({
101
+ payload_off: "false",
102
+ payload_on: "true",
103
+ unique_id: "#{property.device.id}_#{property.node.id}_#{property.id}",
104
+ state_topic: property.topic
105
+ })
106
+ config[:expire_after] = expire_after if expire_after
107
+ config[:force_update] = true if force_update
108
+ config[:off_delay] = off_delay if off_delay
109
+
110
+ publish(property.mqtt, "binary_sensor", config, discovery_prefix: discovery_prefix)
111
+ end
112
+
113
+ def publish_climate(
114
+ action_property: nil,
115
+ aux_property: nil,
116
+ away_mode_property: nil,
117
+ current_temperature_property: nil,
118
+ fan_mode_property: nil,
119
+ mode_property: nil,
120
+ hold_property: nil,
121
+ power_property: nil,
122
+ swing_mode_property: nil,
123
+ temperature_property: nil,
124
+ temperature_high_property: nil,
125
+ temperature_low_property: nil,
126
+ name: nil,
127
+ id: nil,
128
+ precision: nil,
129
+ temp_step: nil,
130
+
131
+ device: nil,
132
+ discovery_prefix: nil,
133
+ entity_category: nil,
134
+ icon: nil,
135
+ templates: {}
136
+ )
137
+ properties = {
138
+ action: action_property,
139
+ aux: aux_property,
140
+ away_mode: away_mode_property,
141
+ current_temperature: current_temperature_property,
142
+ fan_mode: fan_mode_property,
143
+ mode: mode_property,
144
+ hold: hold_property,
145
+ power: power_property,
146
+ swing_mode: swing_mode_property,
147
+ temperature: temperature_property,
148
+ temperature_high: temperature_high_property,
149
+ temperature_low: temperature_low_property
150
+ }.compact
151
+ raise ArgumentError, "At least one property must be specified" if properties.empty?
152
+ raise ArgumentError, "Power property must be a boolean" if power_property && power_property.datatype != :boolean
153
+
154
+ node = properties.first.last.node
155
+
156
+ config = base_config(node.device,
157
+ name || node.full_name,
158
+ device: device,
159
+ entity_category: entity_category,
160
+ icon: icon)
161
+ config[:unique_id] = "#{node.device.id}_#{id || node.id}"
162
+ properties.each do |prefix, property|
163
+ add_property(config, property, prefix, templates)
164
+ end
165
+ temp_properties = [
166
+ temperature_property,
167
+ temperature_high_property,
168
+ temperature_low_property
169
+ ].compact
170
+ unless (temp_ranges = temp_properties.map(&:range).compact).empty?
171
+ config[:min_temp] = temp_ranges.map(&:begin).min
172
+ config[:max_temp] = temp_ranges.map(&:end).max
173
+ end
174
+ temperature_unit = temp_properties.map(&:unit).compact.first
175
+ config[:temperature_unit] = temperature_unit[-1] if temperature_unit
176
+ {
177
+ nil => mode_property,
178
+ :fan => fan_mode_property,
179
+ :hold => hold_property,
180
+ :swing => swing_mode_property
181
+ }.compact.each do |prefix, property|
182
+ valid_set = %w[auto off cool heat dry fan_only] if prefix.nil?
183
+ add_enum(config, property, prefix, valid_set)
184
+ end
185
+ config[:precision] = precision if precision
186
+ config[:temp_step] = temp_step if temp_step
187
+ if power_property
188
+ config[:payload_on] = "true"
189
+ config[:payload_off] = "false"
190
+ end
191
+
192
+ publish(node.mqtt, "climate", config, discovery_prefix: discovery_prefix)
193
+ end
194
+
195
+ def publish_fan(
196
+ property,
197
+ oscillation_property: nil,
198
+ percentage_property: nil,
199
+ preset_mode_property: nil,
200
+
201
+ device: nil,
202
+ discovery_prefix: nil,
203
+ entity_category: nil,
204
+ icon: nil
205
+ )
206
+ config = base_config(property.device,
207
+ name || property.node.full_name,
208
+ device: device,
209
+ device_class: device_class,
210
+ entity_category: entity_category,
211
+ icon: icon,
212
+ templates: {})
213
+ add_property(config, oscillation_property, :oscillation_property, templates)
214
+ add_property(config, percentage_property, :percentage, templates)
215
+ if percentage_property&.range
216
+ config[:speed_range_min] = percentage_property.range.begin
217
+ config[:speed_range_max] = percentage_property.range.end
218
+ end
219
+ add_property(config, preset_mode_property, :preset, templates)
220
+ add_enum(config, preset_mode_property, :preset)
221
+
222
+ publish(node.mqtt, "fan", config, discovery_prefix: discovery_prefix)
223
+ end
224
+
225
+ def publish_humidifier(
226
+ property,
227
+ device_class:,
228
+ target_property:,
229
+ mode_property: nil,
230
+ name: nil,
231
+ id: nil,
232
+
233
+ device: nil,
234
+ discovery_prefix: nil,
235
+ entity_category: nil,
236
+ icon: nil
237
+ )
238
+ raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
239
+
240
+ unless DEVICE_CLASSES[:humidifier].include?(device_class)
241
+ raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
242
+ end
243
+
244
+ config = base_config(property.device,
245
+ name || property.node.full_name,
246
+ device: device,
247
+ device_class: device_class,
248
+ entity_category: entity_category,
249
+ icon: icon)
250
+ .merge({
251
+ command_topic: "#{property.topic}/set",
252
+ target_humidity_command_topic: "#{target_property.topic}/set",
253
+ payload_off: "false",
254
+ payload_on: "true",
255
+ unique_id: "#{property.device.id}_#{id || property.node.id}"
256
+ })
257
+ add_property(config, property)
258
+ add_property(config, target_property, :target_humidity)
259
+ if (range = target_property.range)
260
+ config[:min_humidity] = range.begin
261
+ config[:max_humidity] = range.end
262
+ end
263
+ add_property(config, mode_property, :mode)
264
+ add_enum(config, mode_property)
265
+
266
+ publish(property.mqtt, "humidifier", config, discovery_prefix: discovery_prefix)
267
+ end
268
+
269
+ # `default` schema only for now
270
+ def publish_light(
271
+ property = nil,
272
+ brightness_property: nil,
273
+ color_mode_property: nil,
274
+ color_temp_property: nil,
275
+ effect_property: nil,
276
+ hs_property: nil,
277
+ rgb_property: nil,
278
+ white_property: nil,
279
+ xy_property: nil,
280
+ on_command_type: nil,
281
+
282
+ device: nil,
283
+ discovery_prefix: nil,
284
+ entity_category: nil,
285
+ icon: nil,
286
+ templates: {}
287
+ )
288
+ if on_command_type && !ON_COMMAND_TYPES.include?(on_command_type)
289
+ raise ArgumentError, "Invalid on_command_type #{on_command_type.inspect}"
290
+ end
291
+
292
+ # automatically infer a brightness-only light and adjust config
293
+ if brightness_property && property.nil?
294
+ property = brightness_property
295
+ on_command_type = :brightness
296
+ end
297
+
298
+ config = base_config(property.device,
299
+ property.full_name,
300
+ device: device,
301
+ entity_category: entity_category,
302
+ icon: icon)
303
+ add_property(config, property)
304
+ case property.datatype
305
+ when :boolean
306
+ config[:payload_off] = "false"
307
+ config[:payload_on] = "true"
308
+ when :integer
309
+ config[:payload_off] = "0"
310
+ when :float
311
+ config[:payload_off] = "0.0"
312
+ end
313
+ add_property(config, brightness_property, :brightness, templates)
314
+ config[:brightness_scale] = brightness_property.range.end if brightness_property&.range
315
+ add_property(config, color_mode_property, :color_mode, templates)
316
+ add_property(config, color_temp_property, :color_temp, templates)
317
+ if color_temp_property&.range && color_temp_property.unit == "mired"
318
+ config[:min_mireds] = color_temp_property.range.begin
319
+ config[:max_mireds] = color_temp_property.range.end
320
+ end
321
+ add_property(config, effect_property, :effect, templates)
322
+ config[:effect_list] = effect_property.range if effect_property&.datatype == :enum
323
+ add_property(config, hs_property, :hs, templates)
324
+ add_property(config, rgb_property, :rgb, templates)
325
+ add_property(config, white_property, :white, templates)
326
+ config[:white_scale] = white_property.range.end if white_property&.range
327
+ add_property(config, xy_property, :xy, templates)
328
+ config[:on_command_type] = on_command_type if on_command_type
329
+
330
+ publish(property.mqtt, "light", config, discovery_prefix: discovery_prefix)
331
+ end
332
+
333
+ def publish_number(
334
+ property,
335
+ step: nil,
336
+
337
+ device: nil,
338
+ discovery_prefix: nil,
339
+ entity_category: nil,
340
+ icon: nil
341
+ )
342
+ raise ArgumentError, "Homie property must be an integer or a float" unless %i[integer
343
+ float].include?(property.datatype)
344
+
345
+ config = base_config(property.device,
346
+ property.full_name,
347
+ device: device,
348
+ entity_category: entity_category,
349
+ icon: icon)
350
+ config[:unique_id] = "#{property.device.id}_#{property.node.id}_#{property.id}"
351
+ add_property(config, property)
352
+ config[:unit_of_measurement] = property.unit if property.unit
353
+ if property.range
354
+ config[:min] = property.range.begin
355
+ config[:max] = property.range.end
356
+ end
357
+ config[:step] = step if step
358
+
359
+ publish(property.mqtt, "number", config, discovery_prefix: discovery_prefix)
360
+ end
361
+
362
+ def publish_scene(
363
+ property,
364
+
365
+ device: nil,
366
+ discovery_prefix: nil,
367
+ entity_category: nil,
368
+ icon: nil
369
+ )
370
+ unless property.datatype == :enum && property.range.length == 1
371
+ raise ArgumentError, "Homie property must be an enum with a single value"
372
+ end
373
+
374
+ config = base_config(property.device,
375
+ property.full_name,
376
+ device: device,
377
+ entity_category: entity_category,
378
+ icon: icon)
379
+ config[:unique_id] = "#{property.device.id}_#{property.node.id}_#{property.id}"
380
+ add_property(config, property)
381
+ config[:payload_on] = property.range.first
382
+
383
+ publish(property.mqtt, "scene", config, discovery_prefix: discovery_prefix)
384
+ end
385
+
386
+ def publish_select(
387
+ property,
388
+
389
+ device: nil,
390
+ discovery_prefix: nil,
391
+ entity_category: nil,
392
+ icon: nil
393
+ )
394
+ raise ArgumentError, "Homie property must be an enum" unless property.datatype == :enum
395
+ raise ArgumentError, "Homie property must be settable" unless property.settable?
396
+
397
+ config = base_config(property.device,
398
+ property.full_name,
399
+ device: device,
400
+ entity_category: entity_category,
401
+ icon: icon)
402
+ config[:unique_id] = "#{property.device.id}_#{property.node.id}_#{property.id}"
403
+ add_property(config, property)
404
+ config[:options] = property.range
405
+
406
+ publish(property.mqtt, "select", config, discovery_prefix: discovery_prefix)
407
+ end
408
+
409
+ # @param property [MQTT::Homie::Property] A Homie property object
410
+ def publish_sensor(
411
+ property,
412
+ device_class: nil,
413
+ expire_after: nil,
414
+ force_update: false,
415
+ state_class: nil,
416
+
417
+ device: nil,
418
+ discovery_prefix: nil,
419
+ entity_category: nil,
420
+ icon: nil
421
+ )
422
+ if device_class && !DEVICE_CLASSES[:sensor].include?(device_class)
423
+ raise ArgumentError, "Unrecognized device_class #{device_class.inspect}"
424
+ end
425
+ if state_class && !STATE_CLASSES.include?(state_class)
426
+ raise ArgumentError, "Unrecognized state_class #{state_class.inspect}"
427
+ end
428
+
429
+ config = base_config(property.device,
430
+ property.full_name,
431
+ device: device,
432
+ device_class: device_class,
433
+ entity_category: entity_category,
434
+ icon: icon)
435
+ .merge({
436
+ unique_id: "#{property.device.id}_#{property.node.id}_#{property.id}",
437
+ state_topic: property.topic
438
+ })
439
+ config[:state_class] = state_class if state_class
440
+ config[:expire_after] = expire_after if expire_after
441
+ config[:force_update] = true if force_update
442
+ config[:unit_of_measurement] = property.unit if property.unit
443
+
444
+ publish(property.mqtt, "sensor", config, discovery_prefix: discovery_prefix)
445
+ end
446
+
447
+ # @param property [MQTT::Homie::Property] A Homie property object of datatype :boolean
448
+ def publish_switch(property,
449
+ device_class: nil,
450
+
451
+ device: nil,
452
+ discovery_prefix: nil,
453
+ entity_category: nil,
454
+ icon: nil)
455
+ raise ArgumentError, "Homie property must be a boolean" unless property.datatype == :boolean
456
+
457
+ config = base_config(property.device,
458
+ property.full_name,
459
+ device: device,
460
+ device_class: device_class,
461
+ entity_category: entity_category,
462
+ icon: icon)
463
+ .merge({
464
+ unique_id: "#{property.device.id}_#{property.node.id}_#{property.id}",
465
+ payload_off: "false",
466
+ payload_on: "true"
467
+ })
468
+ add_property(config, property)
469
+
470
+ publish(property.mqtt, "switch", config, discovery_prefix: discovery_prefix)
471
+ end
472
+
473
+ private
474
+
475
+ def add_property(config, property, prefix = nil, templates = {})
476
+ return unless property
477
+
478
+ prefix = "#{prefix}_" if prefix
479
+ config[:"#{prefix}state_topic"] = property.topic if property.retained?
480
+ if property.settable?
481
+ config[:"#{prefix}command_topic"] = "#{property.topic}/set"
482
+ config[:"#{prefix}command_template"] = "{{ value | round(0) }}" if property.datatype == :integer
483
+ end
484
+ config.merge!(templates.slice(:"#{prefix}_template", :"#{prefix}_command_template"))
485
+ end
486
+
487
+ def add_enum(config, property, prefix = nil, valid_set = nil)
488
+ prefix = "#{prefix}_" if prefix
489
+
490
+ return unless property&.datatype == :enum
491
+
492
+ modes = property.range
493
+ modes &= valid_set if valid_set
494
+ config[:"#{prefix}modes"] = modes
495
+ end
496
+
497
+ def base_config(homie_device,
498
+ name,
499
+ device:,
500
+ entity_category:,
501
+ icon:,
502
+ device_class: nil)
503
+ if entity_category && !ENTITY_CATEGORIES.include?(entity_category)
504
+ raise ArgumentError, "Unrecognized entity_category #{entity_category.inspect}"
505
+ end
506
+
507
+ config = {
508
+ name: name,
509
+ availability_topic: "#{homie_device.topic}/$state",
510
+ payload_available: "ready",
511
+ payload_not_available: "lost",
512
+ qos: 1
513
+ }
514
+ config[:device_class] = device_class if device_class
515
+ config[:entity_category] = entity_category if entity_category
516
+ config[:icon] = icon if icon
517
+
518
+ device = device&.dup || {}
519
+ device[:name] ||= homie_device.name
520
+ device[:sw_version] ||= MQTT::Homie::Device::VERSION
521
+ device[:identifiers] ||= homie_device.id unless device[:connections]
522
+ config[:device] = device
523
+
524
+ config
525
+ end
526
+
527
+ def publish(mqtt, component, config, discovery_prefix:)
528
+ mqtt.publish("#{discovery_prefix || "homeassistant"}/#{component}/#{config[:unique_id]}/config",
529
+ config.to_json,
530
+ retain: true,
531
+ qos: 1)
532
+ end
533
+ end
534
+ end
535
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mqtt/home_assistant"
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mqtt-homeassistant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Cody Cutrer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-12-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: homie-mqtt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '11.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '11.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.23'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.23'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-performance
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.12'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.12'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.6'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.6'
111
+ description:
112
+ email: cody@cutrer.com'
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - lib/mqtt-homeassistant.rb
118
+ - lib/mqtt/home_assistant.rb
119
+ - lib/mqtt/home_assistant/version.rb
120
+ homepage: https://github.com/ccutrer/ruby-mqtt-homeassistant
121
+ licenses:
122
+ - MIT
123
+ metadata:
124
+ rubygems_mfa_required: 'true'
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '2.5'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubygems_version: 3.1.2
141
+ signing_key:
142
+ specification_version: 4
143
+ summary: Library for publishing device auto-discovery configuration for Home Assistant
144
+ via MQTT.
145
+ test_files: []