homie-mqtt 1.0.1 → 1.3.0

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