homie-mqtt 1.0.1 → 1.2.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: fd6f3bb8678e0d56ddc578993ba46f8bafb3361e4d5f7b0d5fbede478e640edc
4
+ data.tar.gz: 4961a257ee03d29d623c009dad99f54ac44ee6701fa1364d2b83d6ce44a3898d
5
5
  SHA512:
6
- metadata.gz: aaf9bb9ad0d8e5a7c3292570885dd8933325b3127ab1938190e2def744bcb95d95c5f1057f9338f90b3db85d4e8091c0b32a9de835fd20426945f0b1831b5820
7
- data.tar.gz: 713b2a20311422b8a8d6e75a4a9ce0c00ae18a556862b38f064f289927c2be364fecfa84450dab1e5120d85e5e2a8d9969c95988a44ab05780db5e3064d547d7
6
+ metadata.gz: 5aedd7bde7d6cc07a8837802eaeef6f563d4d846b912f737df888a8d2d893bbe2556191f7e2847f2a903527b5c3d7342f62d63562175eac7207dde46867877b1
7
+ data.tar.gz: dfaa32de5c1126801ceeb164fe919a35c266fec727096539e6e3e794c19d380852c0a910a090222ba5d3396edf7a5a25522d25396453c6734cf5ff33b88ff36d
@@ -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
@@ -36,46 +48,59 @@ module MQTT
36
48
  yield node if block_given?
37
49
  if prior_state == :ready
38
50
  node.publish
39
- mqtt.publish("#{topic}/$nodes", nodes.keys.join(","), true, 1)
51
+ mqtt.publish("#{topic}/$nodes", @nodes.keys.join(","), retain: true, qos: 1)
40
52
  end
41
53
  end
42
54
  self
43
55
  end
44
56
 
45
57
  def remove_node(id)
46
- return unless (node = nodes[id])
58
+ return unless (node = @nodes[id])
47
59
  init do
48
60
  node.unpublish
49
61
  @nodes.delete(id)
50
- mqtt.publish("#{topic}/$nodes", nodes.keys.join(","), true, 1) if @published
62
+ mqtt.publish("#{topic}/$nodes", @nodes.keys.join(","), retain: true, qos: 1) if @published
51
63
  end
52
64
  end
53
65
 
66
+ def [](id)
67
+ @nodes[id]
68
+ end
69
+
70
+ def each(&block)
71
+ @nodes.each_value(&block)
72
+ end
73
+
54
74
  def publish
55
75
  return if @published
56
76
 
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)
77
+ mqtt.batch_publish do
78
+ mqtt.publish("#{topic}/$homie", "4.0.0", retain: true, qos: 1)
79
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
80
+ mqtt.publish("#{topic}/$state", @state.to_s, retain: true, qos: 1)
60
81
 
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
82
+ @subscription_thread = Thread.new do
83
+ # you'll get the exception when you call `join`
84
+ Thread.current.report_on_exception = false
66
85
 
67
- unless property&.settable?
68
- @block&.call(topic, value)
69
- next
70
- end
86
+ mqtt.get do |packet|
87
+ match = packet.topic.match(topic_regex)
88
+ node = @nodes[match[:node]] if match
89
+ property = node[match[:property]] if node
71
90
 
72
- property.set(value)
91
+ unless property&.settable?
92
+ @block&.call(topic, packet.payload)
93
+ next
94
+ end
95
+
96
+ property.set(packet.payload)
97
+ end
73
98
  end
99
+
100
+ mqtt.publish("#{topic}/$nodes", @nodes.keys.join(","), retain: true, qos: 1)
101
+ @nodes.each_value(&:publish)
102
+ mqtt.publish("#{topic}/$state", (@state = :ready).to_s, retain: true, qos: 1)
74
103
  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
104
 
80
105
  @published = true
81
106
  end
@@ -88,6 +113,9 @@ module MQTT
88
113
 
89
114
  def join
90
115
  @subscription_thread&.join
116
+ rescue => e
117
+ e.set_backtrace(e.backtrace + ["<from Homie MQTT thread>"] + caller)
118
+ raise e
91
119
  end
92
120
 
93
121
  def init
@@ -97,14 +125,23 @@ module MQTT
97
125
  end
98
126
 
99
127
  prior_state = state
100
- mqtt.publish("#{topic}/$state", (state = :init).to_s, true, 1)
128
+ mqtt.publish("#{topic}/$state", (state = :init).to_s, retain: true, qos: 1)
101
129
  yield(prior_state)
102
- mqtt.publish("#{topic}/$state", (state = :ready).to_s, true, 1)
130
+ mqtt.publish("#{topic}/$state", (state = :ready).to_s, retain: true, qos: 1)
103
131
  nil
104
132
  end
105
133
 
106
134
  private
107
135
 
136
+ def clear_topics
137
+ @mqtt.subscribe("#{topic}/#")
138
+ @mqtt.unsubscribe("#{topic}/#", wait_for_ack: true)
139
+ while !@mqtt.queue_empty?
140
+ packet = @mqtt.get
141
+ @mqtt.publish(packet.topic, retain: true, qos: 0)
142
+ end
143
+ end
144
+
108
145
  def topic_regex
109
146
  @topic_regex ||= Regexp.new("^#{Regexp.escape(topic)}/(?<node>#{REGEX})/(?<property>#{REGEX})/set$")
110
147
  end
@@ -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)
@@ -28,34 +28,48 @@ module MQTT
28
28
  end
29
29
 
30
30
  def remove_property(id)
31
- return unless (property = properties[id])
31
+ return 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
37
  end
38
38
 
39
+ def [](id)
40
+ @properties[id]
41
+ end
42
+
43
+ def each(&block)
44
+ @properties.each_value(&block)
45
+ end
46
+
39
47
  def mqtt
40
48
  device.mqtt
41
49
  end
42
50
 
43
51
  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
52
+ mqtt.batch_publish do
53
+ unless @published
54
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
55
+ mqtt.publish("#{topic}/$type", @type.to_s, retain: true, qos: 1)
56
+ @published = true
57
+ end
58
+
59
+ mqtt.publish("#{topic}/$properties", @properties.keys.join(","), retain: true, qos: 1)
60
+ @properties.each_value(&:publish)
48
61
  end
49
-
50
- mqtt.publish("#{topic}/$properties", properties.keys.join(","), true, 1)
51
- properties.each_value(&:publish)
52
62
  end
53
63
 
54
64
  def unpublish
55
65
  return unless @published
56
66
  @published = false
57
67
 
58
- properties.each_value(&:unpublish)
68
+ mqtt.publish("#{topic}/$name", retain: true, qos: 0)
69
+ mqtt.publish("#{topic}/$type", retain: true, qos: 0)
70
+ mqtt.publish("#{topic}/$properties", retain: true, qos: 0)
71
+
72
+ @properties.each_value(&:unpublish)
59
73
  end
60
74
  end
61
75
  end
@@ -8,9 +8,19 @@ 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
+
13
22
  super(id, name)
23
+
14
24
  @node = node
15
25
  @datatype = datatype
16
26
  @format = format
@@ -40,7 +50,7 @@ module MQTT
40
50
  def value=(value)
41
51
  if @value != value
42
52
  @value = value
43
- mqtt.publish(topic, value.to_s, retained?, 1) if @published
53
+ mqtt.publish(topic, value.to_s, retain: retained?, qos: 1) if @published
44
54
  end
45
55
  end
46
56
 
@@ -49,7 +59,7 @@ module MQTT
49
59
  @unit = unit
50
60
  if @published
51
61
  device.init do
52
- mqtt.publish("#{topic}/$unit", unit.to_s, true, 1)
62
+ mqtt.publish("#{topic}/$unit", unit.to_s, retain: true, qos: 1)
53
63
  end
54
64
  end
55
65
  end
@@ -60,14 +70,47 @@ module MQTT
60
70
  @format = format
61
71
  if @published
62
72
  device.init do
63
- mqtt.publish("#{topic}/$format", format.to_s, true, 1)
73
+ mqtt.publish("#{topic}/$format", format.to_s, retain: true, qos: 1)
64
74
  end
65
75
  end
66
76
  end
67
77
  end
68
78
 
79
+ def range
80
+ case datatype
81
+ when :enum; format.split(',')
82
+ when :integer; Range.new(*format.split(':').map(&:to_i))
83
+ when :float; Range.new(*format.split(':').map(&:to_f))
84
+ else; raise MethodNotImplemented
85
+ end
86
+ end
87
+
69
88
  def set(value)
70
- @block.call(self, value)
89
+ case datatype
90
+ when :boolean
91
+ return unless %w{true false}.include?(value)
92
+ value = value == 'true'
93
+ when :integer
94
+ return unless value =~ /^-?\d+$/
95
+ value = value.to_i
96
+ return unless range.include?(value) if format
97
+ when :float
98
+ return unless value =~ /^-?(?:\d+|\d+\.|\.\d+|\d+\.\d+)(?:[eE]-?\d+)?$/
99
+ value = value.to_f
100
+ return unless range.include?(value) if format
101
+ when :enum
102
+ return unless range.include?(value)
103
+ when :color
104
+ return unless value =~ /^\d{1,3},\d{1,3},\d{1,3}$/
105
+ value = value.split(',').map(&:to_i)
106
+ if format == 'rgb'
107
+ return if value.max > 255
108
+ elsif format == 'hsv'
109
+ return if value.first > 360 || value[1..2].max > 100
110
+ end
111
+ end
112
+
113
+ @block.arity == 2 ? @block.call(value, self) : @block.call(value)
71
114
  end
72
115
 
73
116
  def mqtt
@@ -77,23 +120,36 @@ module MQTT
77
120
  def publish
78
121
  return if @published
79
122
 
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
123
+ mqtt.batch_publish do
124
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
125
+ mqtt.publish("#{topic}/$datatype", datatype.to_s, retain: true, qos: 1)
126
+ mqtt.publish("#{topic}/$format", format, retain: true, qos: 1) if format
127
+ mqtt.publish("#{topic}/$settable", "true", retain: true, qos: 1) if settable?
128
+ mqtt.publish("#{topic}/$retained", "false", retain: true, qos: 1) unless retained?
129
+ mqtt.publish("#{topic}/$unit", unit, retain: true, qos: 1) if unit
130
+ mqtt.publish(topic, value.to_s, retain: retained?, qos: 1) if value
131
+ subscribe
132
+ end
88
133
 
89
134
  @published = true
90
135
  end
91
136
 
137
+ def subscribe
138
+ mqtt.subscribe("#{topic}/set") if settable?
139
+ end
140
+
92
141
  def unpublish
93
142
  return unless @published
94
143
  @published = false
95
144
 
145
+ mqtt.publish("#{topic}/$name", retain: true, qos: 0)
146
+ mqtt.publish("#{topic}/$datatype", retain: true, qos: 0)
147
+ mqtt.publish("#{topic}/$format", retain: true, qos: 0) if format
148
+ mqtt.publish("#{topic}/$settable", retain: true, qos: 0) if settable?
149
+ mqtt.publish("#{topic}/$retained", retain: true, qos: 0) unless retained?
150
+ mqtt.publish("#{topic}/$unit", retain: true, qos: 0) if unit
96
151
  mqtt.unsubscribe("#{topic}/set") if settable?
152
+ mqtt.publish(topic, retain: retained?, qos: 0) if value && retained?
97
153
  end
98
154
  end
99
155
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MQTT
4
4
  module Homie
5
- VERSION = '1.0.1'
5
+ VERSION = '1.2.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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-27 00:00:00.000000000 Z
11
+ date: 2021-02-20 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