homie-mqtt 1.0.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c0070a3f65a2d335b3ecaa206e3969a024db3218b8eddb91b03434f0953c712
4
- data.tar.gz: d12edaebbf8d5da85e2c67b2ca8c64068daff04bd588770086bcb1d0b24892ea
3
+ metadata.gz: 64cfaf8cbbff82a3059c8fcfa639efc39f006241cd79aaadb6bce200eb82a9cc
4
+ data.tar.gz: d9045a9c729515c2912aeb11b20c5ecad9885b7e0f4df95263e688d74d72f077
5
5
  SHA512:
6
- metadata.gz: aaf9bb9ad0d8e5a7c3292570885dd8933325b3127ab1938190e2def744bcb95d95c5f1057f9338f90b3db85d4e8091c0b32a9de835fd20426945f0b1831b5820
7
- data.tar.gz: 713b2a20311422b8a8d6e75a4a9ce0c00ae18a556862b38f064f289927c2be364fecfa84450dab1e5120d85e5e2a8d9969c95988a44ab05780db5e3064d547d7
6
+ metadata.gz: 04ea1126fe4310cc758ddf23fe859f380a0250baf0998a1fab3d695c6f18e6087028dd35de8f619414ec0512d46ac24fb6fb91cae2821729c18b2a91b209455d
7
+ data.tar.gz: 6200b420bed16a9267cdde8f7192c52d8cd72568dcb72e35247620bd4b71031069a12986b41290329c199bfbf0fc7abdbf25a81d8fdbe1bcce7906b09a883673
@@ -18,7 +18,7 @@ module MQTT
18
18
  name = value
19
19
  if @published
20
20
  device.init do
21
- mqtt.publish("#{topic}/$name", name, true, 1)
21
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
22
22
  end
23
23
  end
24
24
  end
@@ -5,9 +5,9 @@ require 'mqtt'
5
5
  module MQTT
6
6
  module Homie
7
7
  class Device < Base
8
- attr_reader :root_topic, :state, :nodes, :mqtt
8
+ attr_reader :root_topic, :state, :mqtt
9
9
 
10
- def initialize(id, name, root_topic: nil, mqtt: nil, &block)
10
+ def initialize(id, name, root_topic: nil, mqtt: nil, clear_topics: true, &block)
11
11
  super(id, name)
12
12
  @root_topic = @root_topic || "homie"
13
13
  @state = :init
@@ -16,8 +16,20 @@ module MQTT
16
16
  @block = block
17
17
  mqtt = MQTT::Client.new(mqtt) if mqtt.is_a?(String)
18
18
  @mqtt = mqtt || MQTT::Client.new
19
- @mqtt.set_will("#{topic}/$state", "lost", true)
19
+ @mqtt.set_will("#{topic}/$state", "lost", retain: true, qos: 1)
20
+
21
+ @mqtt.on_reconnect do
22
+ each do |node|
23
+ node.each do |property|
24
+ property.subscribe
25
+ end
26
+ end
27
+ mqtt.publish("#{topic}/$state", :init, retain: true, qos: 1)
28
+ mqtt.publish("#{topic}/$state", state, retain: true, qos: 1) unless state == :init
29
+ end
30
+
20
31
  @mqtt.connect
32
+ self.clear_topics if clear_topics
21
33
  end
22
34
 
23
35
  def device
@@ -28,54 +40,70 @@ module MQTT
28
40
  "#{root_topic}/#{id}"
29
41
  end
30
42
 
31
- def node(*args, **kwargs)
43
+ def node(id, *args, **kwargs)
44
+ raise ArgumentError, "Node '#{id}' already exists" if @nodes.key?(id)
45
+
32
46
  init do |prior_state|
33
- node = Node.new(self, *args, **kwargs)
34
- raise ArgumentError, "Node '#{node.id}' already exists" if @nodes.key?(node.id)
35
- @nodes[node.id] = node
47
+ node = Node.new(self, id, *args, **kwargs)
48
+
49
+ @nodes[id] = node
36
50
  yield node if block_given?
37
51
  if prior_state == :ready
38
52
  node.publish
39
- mqtt.publish("#{topic}/$nodes", nodes.keys.join(","), true, 1)
53
+ mqtt.publish("#{topic}/$nodes", @nodes.keys.join(","), retain: true, qos: 1)
40
54
  end
55
+ node
41
56
  end
42
- self
43
57
  end
44
58
 
45
59
  def remove_node(id)
46
- return unless (node = nodes[id])
60
+ return false unless (node = @nodes[id])
47
61
  init do
48
62
  node.unpublish
49
63
  @nodes.delete(id)
50
- mqtt.publish("#{topic}/$nodes", nodes.keys.join(","), true, 1) if @published
64
+ mqtt.publish("#{topic}/$nodes", @nodes.keys.join(","), retain: true, qos: 1) if @published
51
65
  end
66
+ true
67
+ end
68
+
69
+ def [](id)
70
+ @nodes[id]
71
+ end
72
+
73
+ def each(&block)
74
+ @nodes.each_value(&block)
52
75
  end
53
76
 
54
77
  def publish
55
78
  return if @published
56
79
 
57
- mqtt.publish("#{topic}/$homie", "4.0.0", true, 1)
58
- mqtt.publish("#{topic}/$name", name, true, 1)
59
- mqtt.publish("#{topic}/$state", @state.to_s, true, 1)
80
+ mqtt.batch_publish do
81
+ mqtt.publish("#{topic}/$homie", "4.0.0", retain: true, qos: 1)
82
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
83
+ mqtt.publish("#{topic}/$state", @state.to_s, retain: true, qos: 1)
60
84
 
61
- @subscription_thread = Thread.new do
62
- mqtt.get do |topic, value|
63
- match = topic.match(topic_regex)
64
- node = nodes[match[:node]] if match
65
- property = node.properties[match[:property]] if node
85
+ @subscription_thread = Thread.new do
86
+ # you'll get the exception when you call `join`
87
+ Thread.current.report_on_exception = false
66
88
 
67
- unless property&.settable?
68
- @block&.call(topic, value)
69
- next
70
- end
89
+ mqtt.get do |packet|
90
+ match = packet.topic.match(topic_regex)
91
+ node = @nodes[match[:node]] if match
92
+ property = node[match[:property]] if node
93
+
94
+ unless property&.settable?
95
+ @block&.call(topic, packet.payload)
96
+ next
97
+ end
71
98
 
72
- property.set(value)
99
+ property.set(packet.payload)
100
+ end
73
101
  end
102
+
103
+ mqtt.publish("#{topic}/$nodes", @nodes.keys.join(","), retain: true, qos: 1)
104
+ @nodes.each_value(&:publish)
105
+ mqtt.publish("#{topic}/$state", (@state = :ready).to_s, retain: true, qos: 1)
74
106
  end
75
-
76
- mqtt.publish("#{topic}/$nodes", nodes.keys.join(","), true, 1)
77
- nodes.each_value(&:publish)
78
- mqtt.publish("#{topic}/$state", (@state = :ready).to_s, true, 1)
79
107
 
80
108
  @published = true
81
109
  end
@@ -88,19 +116,36 @@ module MQTT
88
116
 
89
117
  def join
90
118
  @subscription_thread&.join
119
+ rescue => e
120
+ e.set_backtrace(e.backtrace + ["<from Homie MQTT thread>"] + caller)
121
+ raise e
91
122
  end
92
123
 
93
124
  def init
94
125
  if state == :init
95
- yield(state)
96
- return
126
+ return yield state
97
127
  end
98
128
 
99
129
  prior_state = state
100
- mqtt.publish("#{topic}/$state", (state = :init).to_s, true, 1)
101
- yield(prior_state)
102
- mqtt.publish("#{topic}/$state", (state = :ready).to_s, true, 1)
103
- nil
130
+ mqtt.publish("#{topic}/$state", (state = :init).to_s, retain: true, qos: 1)
131
+ result = nil
132
+ mqtt.batch_publish do
133
+ result = yield prior_state
134
+ end
135
+ mqtt.publish("#{topic}/$state", (state = :ready).to_s, retain: true, qos: 1)
136
+ result
137
+ end
138
+
139
+ def clear_topics
140
+ raise ArgumentError, "cannot clear topics once published" if @published
141
+
142
+ @mqtt.subscribe("#{topic}/#")
143
+ @mqtt.unsubscribe("#{topic}/#", wait_for_ack: true)
144
+ while !@mqtt.queue_empty?
145
+ packet = @mqtt.get
146
+ @mqtt.publish(packet.topic, retain: true, qos: 0)
147
+ end
148
+ true
104
149
  end
105
150
 
106
151
  private
@@ -3,7 +3,7 @@
3
3
  module MQTT
4
4
  module Homie
5
5
  class Node < Base
6
- attr_reader :device, :type, :properties
6
+ attr_reader :device, :type
7
7
 
8
8
  def initialize(device, id, name, type)
9
9
  super(id, name)
@@ -23,39 +23,63 @@ module MQTT
23
23
  raise ArgumentError, "Property '#{property.id}' already exists on '#{id}'" if @properties.key?(property.id)
24
24
  @properties[property.id] = property
25
25
  property.publish if prior_state == :ready
26
+ property
26
27
  end
27
- self
28
28
  end
29
29
 
30
30
  def remove_property(id)
31
- return unless (property = properties[id])
31
+ return false unless (property = @properties[id])
32
32
  init do
33
33
  property.unpublish
34
34
  @properties.delete(id)
35
- mqtt.publish("#{topic}/$properties", properties.keys.join(","), true, 1) if @published
35
+ mqtt.publish("#{topic}/$properties", @properties.keys.join(","), retain: true, qos: 1) if @published
36
36
  end
37
+ true
38
+ end
39
+
40
+ def [](id)
41
+ @properties[id]
42
+ end
43
+
44
+ def each(&block)
45
+ @properties.each_value(&block)
37
46
  end
38
47
 
39
48
  def mqtt
40
49
  device.mqtt
41
50
  end
42
51
 
52
+ # takes a hash with property names as keys, and values as values
53
+ def batch_update(hash)
54
+ mqtt.batch_publish do
55
+ hash.each do |(k, v)|
56
+ self[k].value = v
57
+ end
58
+ end
59
+ end
60
+
43
61
  def publish
44
- unless @published
45
- mqtt.publish("#{topic}/$name", name, true, 1)
46
- mqtt.publish("#{topic}/$type", @type.to_s, true, 1)
47
- @published = true
62
+ mqtt.batch_publish do
63
+ unless @published
64
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
65
+ mqtt.publish("#{topic}/$type", @type.to_s, retain: true, qos: 1)
66
+ @published = true
67
+ end
68
+
69
+ mqtt.publish("#{topic}/$properties", @properties.keys.join(","), retain: true, qos: 1)
70
+ @properties.each_value(&:publish)
48
71
  end
49
-
50
- mqtt.publish("#{topic}/$properties", properties.keys.join(","), true, 1)
51
- properties.each_value(&:publish)
52
72
  end
53
73
 
54
74
  def unpublish
55
75
  return unless @published
56
76
  @published = false
57
77
 
58
- properties.each_value(&:unpublish)
78
+ mqtt.publish("#{topic}/$name", retain: true, qos: 0)
79
+ mqtt.publish("#{topic}/$type", retain: true, qos: 0)
80
+ mqtt.publish("#{topic}/$properties", retain: true, qos: 0)
81
+
82
+ @properties.each_value(&:unpublish)
59
83
  end
60
84
  end
61
85
  end
@@ -8,9 +8,20 @@ module MQTT
8
8
  def initialize(node, id, name, datatype, value = nil, format: nil, retained: true, unit: nil, &block)
9
9
  raise ArgumentError, "Invalid Homie datatype" unless %s{string integer float boolean enum color}
10
10
  raise ArgumentError, "retained must be boolean" unless [true, false].include?(retained)
11
+ format = format.join(",") if format.is_a?(Array) && datatype == :enum
12
+ if %i{integer float}.include?(datatype) && format.is_a?(Range)
13
+ raise ArgumentError "only inclusive ranges are supported" if format.exclude_end?
14
+ format = "#{format.begin}:#{format.end}"
15
+ end
11
16
  raise ArgumentError, "format must be nil or a string" unless format.nil? || format.is_a?(String)
12
17
  raise ArgumentError, "unit must be nil or a string" unless unit.nil? || unit.is_a?(String)
18
+ raise ArgumentError, "format is required for enums" if datatype == :enum && format.nil?
19
+ raise ArgumentError, "format is required for colors" if datatype == :color && format.nil?
20
+ raise ArgumentError, "format must be either rgb or hsv for colors" if datatype == :color && !%w{rgb hsv}.include?(format.to_s)
21
+ raise ArgumentError, "an initial value cannot be provided for a non-retained property" if !value.nil? && !retained
22
+
13
23
  super(id, name)
24
+
14
25
  @node = node
15
26
  @datatype = datatype
16
27
  @format = format
@@ -39,8 +50,8 @@ module MQTT
39
50
 
40
51
  def value=(value)
41
52
  if @value != value
42
- @value = value
43
- mqtt.publish(topic, value.to_s, retained?, 1) if @published
53
+ @value = value if retained?
54
+ mqtt.publish(topic, value.to_s, retain: retained?, qos: 1) if @published
44
55
  end
45
56
  end
46
57
 
@@ -49,7 +60,7 @@ module MQTT
49
60
  @unit = unit
50
61
  if @published
51
62
  device.init do
52
- mqtt.publish("#{topic}/$unit", unit.to_s, true, 1)
63
+ mqtt.publish("#{topic}/$unit", unit.to_s, retain: true, qos: 1)
53
64
  end
54
65
  end
55
66
  end
@@ -60,14 +71,47 @@ module MQTT
60
71
  @format = format
61
72
  if @published
62
73
  device.init do
63
- mqtt.publish("#{topic}/$format", format.to_s, true, 1)
74
+ mqtt.publish("#{topic}/$format", format.to_s, retain: true, qos: 1)
64
75
  end
65
76
  end
66
77
  end
67
78
  end
68
79
 
80
+ def range
81
+ case datatype
82
+ when :enum; format.split(',')
83
+ when :integer; Range.new(*format.split(':').map(&:to_i))
84
+ when :float; Range.new(*format.split(':').map(&:to_f))
85
+ else; raise MethodNotImplemented
86
+ end
87
+ end
88
+
69
89
  def set(value)
70
- @block.call(self, value)
90
+ case datatype
91
+ when :boolean
92
+ return unless %w{true false}.include?(value)
93
+ value = value == 'true'
94
+ when :integer
95
+ return unless value =~ /^-?\d+$/
96
+ value = value.to_i
97
+ return unless range.include?(value) if format
98
+ when :float
99
+ return unless value =~ /^-?(?:\d+|\d+\.|\.\d+|\d+\.\d+)(?:[eE]-?\d+)?$/
100
+ value = value.to_f
101
+ return unless range.include?(value) if format
102
+ when :enum
103
+ return unless range.include?(value)
104
+ when :color
105
+ return unless value =~ /^\d{1,3},\d{1,3},\d{1,3}$/
106
+ value = value.split(',').map(&:to_i)
107
+ if format == 'rgb'
108
+ return if value.max > 255
109
+ elsif format == 'hsv'
110
+ return if value.first > 360 || value[1..2].max > 100
111
+ end
112
+ end
113
+
114
+ @block.arity == 2 ? @block.call(value, self) : @block.call(value)
71
115
  end
72
116
 
73
117
  def mqtt
@@ -77,23 +121,36 @@ module MQTT
77
121
  def publish
78
122
  return if @published
79
123
 
80
- mqtt.publish("#{topic}/$name", name, true, 1)
81
- mqtt.publish("#{topic}/$datatype", datatype.to_s, true, 1)
82
- mqtt.publish("#{topic}/$format", format, true, 1) if format
83
- mqtt.publish("#{topic}/$settable", "true", true, 1) if settable?
84
- mqtt.publish("#{topic}/$retained", "false", true, 1) unless retained?
85
- mqtt.publish("#{topic}/$unit", unit, true, 1) if unit
86
- mqtt.subscribe("#{topic}/set") if settable?
87
- mqtt.publish(topic, value.to_s, retained?, 1) if value
124
+ mqtt.batch_publish do
125
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
126
+ mqtt.publish("#{topic}/$datatype", datatype.to_s, retain: true, qos: 1)
127
+ mqtt.publish("#{topic}/$format", format, retain: true, qos: 1) if format
128
+ mqtt.publish("#{topic}/$settable", "true", retain: true, qos: 1) if settable?
129
+ mqtt.publish("#{topic}/$retained", "false", retain: true, qos: 1) unless retained?
130
+ mqtt.publish("#{topic}/$unit", unit, retain: true, qos: 1) if unit
131
+ mqtt.publish(topic, value.to_s, retain: retained?, qos: 1) unless value.nil?
132
+ subscribe
133
+ end
88
134
 
89
135
  @published = true
90
136
  end
91
137
 
138
+ def subscribe
139
+ mqtt.subscribe("#{topic}/set") if settable?
140
+ end
141
+
92
142
  def unpublish
93
143
  return unless @published
94
144
  @published = false
95
145
 
146
+ mqtt.publish("#{topic}/$name", retain: true, qos: 0)
147
+ mqtt.publish("#{topic}/$datatype", retain: true, qos: 0)
148
+ mqtt.publish("#{topic}/$format", retain: true, qos: 0) if format
149
+ mqtt.publish("#{topic}/$settable", retain: true, qos: 0) if settable?
150
+ mqtt.publish("#{topic}/$retained", retain: true, qos: 0) unless retained?
151
+ mqtt.publish("#{topic}/$unit", retain: true, qos: 0) if unit
96
152
  mqtt.unsubscribe("#{topic}/set") if settable?
153
+ mqtt.publish(topic, retain: retained?, qos: 0) if !value.nil? && retained?
97
154
  end
98
155
  end
99
156
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MQTT
4
4
  module Homie
5
- VERSION = '1.0.1'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homie-mqtt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-27 00:00:00.000000000 Z
11
+ date: 2021-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: mqtt
14
+ name: mqtt-ccutrer
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.5.0
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.5.0
26
+ version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: byebug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '13.0'
55
- description:
55
+ description:
56
56
  email: cody@cutrer.com'
57
57
  executables: []
58
58
  extensions: []
@@ -68,7 +68,7 @@ homepage: https://github.com/ccutrer/homie-mqtt
68
68
  licenses:
69
69
  - MIT
70
70
  metadata: {}
71
- post_install_message:
71
+ post_install_message:
72
72
  rdoc_options: []
73
73
  require_paths:
74
74
  - lib
@@ -83,8 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
83
  - !ruby/object:Gem::Version
84
84
  version: '0'
85
85
  requirements: []
86
- rubygems_version: 3.1.4
87
- signing_key:
86
+ rubygems_version: 3.1.2
87
+ signing_key:
88
88
  specification_version: 4
89
89
  summary: Library for publishing devices that conform to the Homie spec.
90
90
  test_files: []