mqtt-homeassistant 0.0.1

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.
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: []