homie-mqtt 1.4.1 → 1.5.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: 000cc1744d0e9520830ebd44954dd56f68e571803bf82539f3ca47a1d89c6bdf
4
- data.tar.gz: 28c8a7530bda1c962c30918c13bdf9cffa71a99f070833959541dfde9806802b
3
+ metadata.gz: 8552d2aca44043208c3b3cd06fff9d5376515103d7de423926713441acecd6e0
4
+ data.tar.gz: a27b5fe2fe50e2975708c308ed9a0f9b5b7d2469667741fe092210adcda8582a
5
5
  SHA512:
6
- metadata.gz: '02591ff555e0ff41b4b5476bef010da8b5ff6e3e5d665c1f22c6a5b02f85e023ad53f243d04748dd4a186c4e212b81071b89925f1a0932dac7fb1098657e1843'
7
- data.tar.gz: 873266a8d592ad4bc47b4e2a4639b599709c0a98b22756477f9683b4a7450b54ebf3f1b60380a1462d5176749ea56cdd69367c7e086b74cb8173910bac7dde57
6
+ metadata.gz: b10c773d61781b4fb2e5595056e1b5e41373ec579bcf0a152f0c5dc763c3973ad09914964caaaf9c619b079e6cf15f4dc923122caf214f571741b8b6934a8317
7
+ data.tar.gz: dd8069070124dea54d81f3fc0095b9cce1d28d77ed655d7162182c0a11cdfecbb93cc40c97ea3778b3ebd35508cf960fdfcdd2594cac87f3621fdf11e64845c0
data/lib/homie-mqtt.rb CHANGED
@@ -4,13 +4,13 @@ module MQTT
4
4
  module Homie
5
5
  class << self
6
6
  def escape_id(id)
7
- id.downcase.gsub(/[^a-z0-9\-]/, '-').sub(/^[^a-z0-9]+/, '')
7
+ id.downcase.gsub(/[^a-z0-9\-]/, "-").sub(/^[^a-z0-9]+/, "")
8
8
  end
9
9
  end
10
10
  end
11
11
  end
12
12
 
13
- require 'mqtt/homie/base'
14
- require 'mqtt/homie/device'
15
- require 'mqtt/homie/node'
16
- require 'mqtt/homie/property'
13
+ require "mqtt/homie/base"
14
+ require "mqtt/homie/device"
15
+ require "mqtt/homie/node"
16
+ require "mqtt/homie/property"
@@ -9,18 +9,19 @@ module MQTT
9
9
 
10
10
  def initialize(id, name)
11
11
  raise ArgumentError, "Invalid Homie ID '#{id}'" unless id.is_a?(String) && id =~ Regexp.new("^#{REGEX}$")
12
+
12
13
  @id = id
13
14
  @name = name
14
15
  end
15
16
 
16
17
  def name=(value)
17
- if name != value
18
- name = value
19
- if @published
20
- device.init do
21
- mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
22
- end
23
- end
18
+ return if name == value
19
+
20
+ name = value
21
+ return unless @published
22
+
23
+ device.init do
24
+ mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
24
25
  end
25
26
  end
26
27
  end
@@ -1,28 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'mqtt'
3
+ require "mqtt"
4
+ require "ruby2_keywords"
4
5
 
5
6
  module MQTT
6
7
  module Homie
7
8
  class Device < Base
9
+ # the Homie spec version
10
+ VERSION = "4.0.0"
11
+
8
12
  attr_reader :root_topic, :state, :mqtt
13
+ attr_accessor :logger, :out_of_band_topic_proc
9
14
 
10
15
  def initialize(id, name, root_topic: nil, mqtt: nil, clear_topics: true, &block)
11
16
  super(id, name)
12
- @root_topic = @root_topic || "homie"
17
+ @root_topic = root_topic || "homie"
13
18
  @state = :init
14
19
  @nodes = {}
15
20
  @published = false
16
- @block = block
21
+ @out_of_band_topic_proc = block
17
22
  mqtt = MQTT::Client.new(mqtt) if mqtt.is_a?(String)
18
23
  @mqtt = mqtt || MQTT::Client.new
19
24
  @mqtt.set_will("#{topic}/$state", "lost", retain: true, qos: 1)
20
25
 
21
26
  @mqtt.on_reconnect do
22
27
  each do |node|
23
- node.each do |property|
24
- property.subscribe
25
- end
28
+ node.each(&:subscribe)
26
29
  end
27
30
  mqtt.publish("#{topic}/$state", :init, retain: true, qos: 1)
28
31
  mqtt.publish("#{topic}/$state", state, retain: true, qos: 1) unless state == :init
@@ -32,6 +35,10 @@ module MQTT
32
35
  self.clear_topics if clear_topics
33
36
  end
34
37
 
38
+ def inspect
39
+ "#<MQTT::Homie::Device #{topic} name=#{name.inspect}, $state=#{state.inspect}>"
40
+ end
41
+
35
42
  def device
36
43
  self
37
44
  end
@@ -40,11 +47,11 @@ module MQTT
40
47
  "#{root_topic}/#{id}"
41
48
  end
42
49
 
43
- def node(id, *args, **kwargs)
50
+ ruby2_keywords def node(id, *args)
44
51
  raise ArgumentError, "Node '#{id}' already exists" if @nodes.key?(id)
45
52
 
46
53
  init do |prior_state|
47
- node = Node.new(self, id, *args, **kwargs)
54
+ node = Node.new(self, id, *args)
48
55
 
49
56
  @nodes[id] = node
50
57
  yield node if block_given?
@@ -58,6 +65,7 @@ module MQTT
58
65
 
59
66
  def remove_node(id)
60
67
  return false unless (node = @nodes[id])
68
+
61
69
  init do
62
70
  node.unpublish
63
71
  @nodes.delete(id)
@@ -74,11 +82,15 @@ module MQTT
74
82
  @nodes.each_value(&block)
75
83
  end
76
84
 
85
+ def count
86
+ @nodes.count
87
+ end
88
+
77
89
  def publish
78
90
  return if @published
79
91
 
80
92
  mqtt.batch_publish do
81
- mqtt.publish("#{topic}/$homie", "4.0.0", retain: true, qos: 1)
93
+ mqtt.publish("#{topic}/$homie", VERSION, retain: true, qos: 1)
82
94
  mqtt.publish("#{topic}/$name", name, retain: true, qos: 1)
83
95
  mqtt.publish("#{topic}/$state", @state.to_s, retain: true, qos: 1)
84
96
 
@@ -87,12 +99,13 @@ module MQTT
87
99
  Thread.current.report_on_exception = false
88
100
 
89
101
  mqtt.get do |packet|
102
+ logger&.debug("received packet at #{packet.topic} with payload #{packet.payload.inspect}")
90
103
  match = packet.topic.match(topic_regex)
91
104
  node = @nodes[match[:node]] if match
92
105
  property = node[match[:property]] if node
93
106
 
94
107
  unless property&.settable?
95
- @block&.call(topic, packet.payload)
108
+ @out_of_band_topic_proc&.call(packet.topic, packet.payload)
96
109
  next
97
110
  end
98
111
 
@@ -102,6 +115,9 @@ module MQTT
102
115
 
103
116
  mqtt.publish("#{topic}/$nodes", @nodes.keys.join(","), retain: true, qos: 1)
104
117
  @nodes.each_value(&:publish)
118
+
119
+ yield if block_given?
120
+
105
121
  mqtt.publish("#{topic}/$state", (@state = :ready).to_s, retain: true, qos: 1)
106
122
  end
107
123
 
@@ -122,17 +138,15 @@ module MQTT
122
138
  end
123
139
 
124
140
  def init
125
- if state == :init
126
- return yield state
127
- end
141
+ return yield state if state == :init
128
142
 
129
143
  prior_state = state
130
- mqtt.publish("#{topic}/$state", (state = :init).to_s, retain: true, qos: 1)
144
+ mqtt.publish("#{topic}/$state", (self.state = :init).to_s, retain: true, qos: 1)
131
145
  result = nil
132
146
  mqtt.batch_publish do
133
147
  result = yield prior_state
134
148
  end
135
- mqtt.publish("#{topic}/$state", (state = :ready).to_s, retain: true, qos: 1)
149
+ mqtt.publish("#{topic}/$state", (self.state = :ready).to_s, retain: true, qos: 1)
136
150
  result
137
151
  end
138
152
 
@@ -141,7 +155,7 @@ module MQTT
141
155
 
142
156
  @mqtt.subscribe("#{topic}/#")
143
157
  @mqtt.unsubscribe("#{topic}/#", wait_for_ack: true)
144
- while !@mqtt.queue_empty?
158
+ until @mqtt.queue_empty?
145
159
  packet = @mqtt.get
146
160
  @mqtt.publish(packet.topic, retain: true, qos: 0)
147
161
  end
@@ -13,14 +13,25 @@ module MQTT
13
13
  @published = false
14
14
  end
15
15
 
16
+ def inspect
17
+ "#<MQTT::Homie::Node #{topic} name=#{full_name.inspect}, type=#{type.inspect}>"
18
+ end
19
+
20
+ def full_name
21
+ return name if device.count == 1
22
+
23
+ "#{device.name} #{name}"
24
+ end
25
+
16
26
  def topic
17
27
  "#{device.topic}/#{id}"
18
28
  end
19
29
 
20
- def property(*args, **kwargs, &block)
30
+ ruby2_keywords def property(*args, &block)
21
31
  device.init do |prior_state|
22
- property = Property.new(self, *args, **kwargs, &block)
32
+ property = Property.new(self, *args, &block)
23
33
  raise ArgumentError, "Property '#{property.id}' already exists on '#{id}'" if @properties.key?(property.id)
34
+
24
35
  @properties[property.id] = property
25
36
  property.publish if prior_state == :ready
26
37
  property
@@ -29,6 +40,7 @@ module MQTT
29
40
 
30
41
  def remove_property(id)
31
42
  return false unless (property = @properties[id])
43
+
32
44
  init do
33
45
  property.unpublish
34
46
  @properties.delete(id)
@@ -73,6 +85,7 @@ module MQTT
73
85
 
74
86
  def unpublish
75
87
  return unless @published
88
+
76
89
  @published = false
77
90
 
78
91
  mqtt.publish("#{topic}/$name", retain: true, qos: 0)
@@ -6,19 +6,26 @@ module MQTT
6
6
  attr_reader :node, :datatype, :format, :unit, :value
7
7
 
8
8
  def initialize(node, id, name, datatype, value = nil, format: nil, retained: true, unit: nil, &block)
9
- raise ArgumentError, "Invalid Homie datatype" unless %i[string integer float boolean enum color datetime duration].include?(datatype)
9
+ raise ArgumentError, "Invalid Homie datatype" unless %i[string integer float boolean enum color datetime
10
+ duration].include?(datatype)
10
11
  raise ArgumentError, "retained must be boolean" unless [true, false].include?(retained)
12
+
11
13
  format = format.join(",") if format.is_a?(Array) && datatype == :enum
12
- if %i{integer float}.include?(datatype) && format.is_a?(Range)
14
+ if %i[integer float].include?(datatype) && format.is_a?(Range)
13
15
  raise ArgumentError "only inclusive ranges are supported" if format.exclude_end?
16
+
14
17
  format = "#{format.begin}:#{format.end}"
15
18
  end
16
19
  raise ArgumentError, "format must be nil or a string" unless format.nil? || format.is_a?(String)
17
20
  raise ArgumentError, "unit must be nil or a string" unless unit.nil? || unit.is_a?(String)
18
21
  raise ArgumentError, "format is required for enums" if datatype == :enum && format.nil?
19
22
  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
23
+ if datatype == :color && !%w[rgb hsv].include?(format.to_s)
24
+ raise ArgumentError, "format must be either rgb or hsv for colors"
25
+ end
26
+ if !value.nil? && !retained
27
+ raise ArgumentError, "an initial value cannot be provided for a non-retained property"
28
+ end
22
29
 
23
30
  super(id, name)
24
31
 
@@ -32,6 +39,24 @@ module MQTT
32
39
  @block = block
33
40
  end
34
41
 
42
+ def inspect
43
+ result = +"#<MQTT::Homie::Property #{topic} name=#{full_name.inspect}, datatype=#{datatype.inspect}"
44
+ result << ", format=#{format.inspect}" if format
45
+ result << ", unit=#{unit.inspect}" if unit
46
+ result << ", settable=true" if settable?
47
+ result << if retained?
48
+ ", value=#{value.inspect}"
49
+ else
50
+ ", retained=false"
51
+ end
52
+ result << ">"
53
+ result.freeze
54
+ end
55
+
56
+ def full_name
57
+ "#{node.full_name} #{name}"
58
+ end
59
+
35
60
  def device
36
61
  node.device
37
62
  end
@@ -49,39 +74,41 @@ module MQTT
49
74
  end
50
75
 
51
76
  def value=(value)
52
- if @value != value
53
- @value = value if retained?
54
- publish_value if @published
55
- end
77
+ return if @value == value
78
+
79
+ @value = value if retained?
80
+ publish_value if @published
56
81
  end
57
82
 
58
83
  def unit=(unit)
59
- if unit != @unit
60
- @unit = unit
61
- if @published
62
- device.init do
63
- mqtt.publish("#{topic}/$unit", unit.to_s, retain: true, qos: 1)
64
- end
65
- end
84
+ return if unit == @unit
85
+
86
+ @unit = unit
87
+ return unless @published
88
+
89
+ device.init do
90
+ mqtt.publish("#{topic}/$unit", unit.to_s, retain: true, qos: 1)
66
91
  end
67
92
  end
68
93
 
69
94
  def format=(format)
70
- if format != @format
71
- @format = format
72
- if @published
73
- device.init do
74
- mqtt.publish("#{topic}/$format", format.to_s, retain: true, qos: 1)
75
- end
76
- end
95
+ return if format == @format
96
+
97
+ @format = format
98
+ return unless @published
99
+
100
+ device.init do
101
+ mqtt.publish("#{topic}/$format", format.to_s, retain: true, qos: 1)
77
102
  end
78
103
  end
79
104
 
80
105
  def range
106
+ return nil unless format
107
+
81
108
  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))
109
+ when :enum then format.split(",")
110
+ when :integer then Range.new(*format.split(":").map(&:to_i))
111
+ when :float then Range.new(*format.split(":").map(&:to_f))
85
112
  else; raise MethodNotImplemented
86
113
  end
87
114
  end
@@ -89,24 +116,29 @@ module MQTT
89
116
  def set(value)
90
117
  case datatype
91
118
  when :boolean
92
- return unless %w{true false}.include?(value)
93
- value = value == 'true'
119
+ return unless %w[true false].include?(value)
120
+
121
+ value = value == "true"
94
122
  when :integer
95
- return unless value =~ /^-?\d+$/
123
+ return unless /^-?\d+$/.match?(value)
124
+
96
125
  value = value.to_i
97
- return unless range.include?(value) if format
126
+ return if format && !range.include?(value)
98
127
  when :float
99
- return unless value =~ /^-?(?:\d+|\d+\.|\.\d+|\d+\.\d+)(?:[eE]-?\d+)?$/
128
+ return unless /^-?(?:\d+|\d+\.|\.\d+|\d+\.\d+)(?:[eE]-?\d+)?$/.match?(value)
129
+
100
130
  value = value.to_f
101
- return unless range.include?(value) if format
131
+ return if format && !range.include?(value)
102
132
  when :enum
103
133
  return unless range.include?(value)
104
134
  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'
135
+ return unless /^\d{1,3},\d{1,3},\d{1,3}$/.match?(value)
136
+
137
+ value = value.split(",").map(&:to_i)
138
+ case format
139
+ when "rgb"
108
140
  return if value.max > 255
109
- elsif format == 'hsv'
141
+ when "hsv"
110
142
  return if value.first > 360 || value[1..2].max > 100
111
143
  end
112
144
  when :datetime
@@ -153,6 +185,7 @@ module MQTT
153
185
 
154
186
  def unpublish
155
187
  return unless @published
188
+
156
189
  @published = false
157
190
 
158
191
  mqtt.publish("#{topic}/$name", retain: true, qos: 0)
@@ -170,8 +203,10 @@ module MQTT
170
203
  def publish_value
171
204
  serialized = value
172
205
  serialized = serialized&.iso8601 if %i[datetime duration].include?(datatype)
206
+ serialized = serialized.to_s
173
207
 
174
- mqtt.publish(topic, serialized.to_s, retain: retained?, qos: 1)
208
+ node.device.logger&.debug("publishing #{serialized.inspect} to #{topic}")
209
+ mqtt.publish(topic, serialized, retain: retained?, qos: 1)
175
210
  end
176
211
  end
177
212
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MQTT
4
4
  module Homie
5
- VERSION = '1.4.1'
5
+ VERSION = "1.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homie-mqtt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.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: 2021-07-30 00:00:00.000000000 Z
11
+ date: 2021-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mqtt-ccutrer
@@ -17,6 +17,9 @@ dependencies:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.0'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.1
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +27,23 @@ dependencies:
24
27
  - - "~>"
25
28
  - !ruby/object:Gem::Version
26
29
  version: '1.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: ruby2_keywords
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.0.5
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.0.5
27
47
  - !ruby/object:Gem::Dependency
28
48
  name: byebug
29
49
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +72,48 @@ dependencies:
52
72
  - - "~>"
53
73
  - !ruby/object:Gem::Version
54
74
  version: '13.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rubocop
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.23'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.23'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rubocop-performance
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.12'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.12'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rubocop-rake
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.6'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.6'
55
117
  description:
56
118
  email: cody@cutrer.com'
57
119
  executables: []
@@ -67,7 +129,8 @@ files:
67
129
  homepage: https://github.com/ccutrer/homie-mqtt
68
130
  licenses:
69
131
  - MIT
70
- metadata: {}
132
+ metadata:
133
+ rubygems_mfa_required: 'true'
71
134
  post_install_message:
72
135
  rdoc_options: []
73
136
  require_paths:
@@ -76,7 +139,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
76
139
  requirements:
77
140
  - - ">="
78
141
  - !ruby/object:Gem::Version
79
- version: '0'
142
+ version: '2.5'
80
143
  required_rubygems_version: !ruby/object:Gem::Requirement
81
144
  requirements:
82
145
  - - ">="