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 +4 -4
- data/lib/mqtt/homie/base.rb +1 -1
- data/lib/mqtt/homie/device.rb +79 -34
- data/lib/mqtt/homie/node.rb +36 -12
- data/lib/mqtt/homie/property.rb +70 -13
- data/lib/mqtt/homie/version.rb +1 -1
- metadata +10 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64cfaf8cbbff82a3059c8fcfa639efc39f006241cd79aaadb6bce200eb82a9cc
|
4
|
+
data.tar.gz: d9045a9c729515c2912aeb11b20c5ecad9885b7e0f4df95263e688d74d72f077
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04ea1126fe4310cc758ddf23fe859f380a0250baf0998a1fab3d695c6f18e6087028dd35de8f619414ec0512d46ac24fb6fb91cae2821729c18b2a91b209455d
|
7
|
+
data.tar.gz: 6200b420bed16a9267cdde8f7192c52d8cd72568dcb72e35247620bd4b71031069a12986b41290329c199bfbf0fc7abdbf25a81d8fdbe1bcce7906b09a883673
|
data/lib/mqtt/homie/base.rb
CHANGED
data/lib/mqtt/homie/device.rb
CHANGED
@@ -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, :
|
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
|
-
|
35
|
-
@nodes[
|
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.
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
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
|
-
|
102
|
-
mqtt.
|
103
|
-
|
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
|
data/lib/mqtt/homie/node.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module MQTT
|
4
4
|
module Homie
|
5
5
|
class Node < Base
|
6
|
-
attr_reader :device, :type
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
data/lib/mqtt/homie/property.rb
CHANGED
@@ -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
|
-
|
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.
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
data/lib/mqtt/homie/version.rb
CHANGED
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
|
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:
|
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:
|
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:
|
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.
|
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: []
|