homie-mqtt 1.0.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 +7 -0
- data/lib/homie-mqtt.rb +16 -0
- data/lib/mqtt/homie/base.rb +28 -0
- data/lib/mqtt/homie/device.rb +113 -0
- data/lib/mqtt/homie/node.rb +62 -0
- data/lib/mqtt/homie/property.rb +100 -0
- data/lib/mqtt/homie/version.rb +7 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4042205ec4dcc333527273ea2b17ca0864fdc5e39923ab28e3f8244be48670d1
|
4
|
+
data.tar.gz: 606e1ecbd6af409a675f5736397044180d89fb31c297fb131294fb303f7a84aa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f06a1de02ebad8ac94ccb93cbae7be71282a9f01adc90a18af1b2cbc364ec39460ed54d663eba9ecce1d05dea08367682acccbc30eff4e9f199855770bc30984
|
7
|
+
data.tar.gz: 9cdb336a778df3d78740cad7643deeee7cc4a412d2737d895048294f1f3e0ec37c728828e509a143a1d27c331c61f3aa26e826342f544dcebca36b05c9ac9e67
|
data/lib/homie-mqtt.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MQTT
|
4
|
+
module Homie
|
5
|
+
class << self
|
6
|
+
def escape_id(id)
|
7
|
+
id.downcase.gsub(/[^a-z0-9\-]/, '-').sub(/^[^a-z0-9]+/, '')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'mqtt/homie/base'
|
14
|
+
require 'mqtt/homie/device'
|
15
|
+
require 'mqtt/homie/node'
|
16
|
+
require 'mqtt/homie/property'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MQTT
|
4
|
+
module Homie
|
5
|
+
class Base
|
6
|
+
REGEX = "[a-z0-9][a-z0-9\-]*"
|
7
|
+
|
8
|
+
attr_reader :id, :name
|
9
|
+
|
10
|
+
def initialize(id, name)
|
11
|
+
raise ArgumentError, "Invalid Homie ID '#{id}'" unless id.is_a?(String) && id =~ Regexp.new("^#{REGEX}$")
|
12
|
+
@id = id
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
def name=(value)
|
17
|
+
if name != value
|
18
|
+
name = value
|
19
|
+
if @published
|
20
|
+
device.init do
|
21
|
+
mqtt.publish("#{topic}/$name", name, true, 1)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mqtt'
|
4
|
+
|
5
|
+
module MQTT
|
6
|
+
module Homie
|
7
|
+
class Device < Base
|
8
|
+
attr_reader :root_topic, :state, :nodes, :mqtt
|
9
|
+
|
10
|
+
def initialize(id, name, root_topic: nil, mqtt: nil, &block)
|
11
|
+
super(id, name)
|
12
|
+
@root_topic = @root_topic || "homie"
|
13
|
+
@state = :init
|
14
|
+
@nodes = {}
|
15
|
+
@published = false
|
16
|
+
@block = block
|
17
|
+
mqtt = MQTT::Client.new(mqtt) if mqtt.is_a?(String)
|
18
|
+
@mqtt = mqtt || MQTT::Client.new
|
19
|
+
@mqtt.set_will("#{topic}/$state", "lost", true)
|
20
|
+
@mqtt.connect
|
21
|
+
end
|
22
|
+
|
23
|
+
def device
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def topic
|
28
|
+
"#{root_topic}/#{id}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def node(*args, **kwargs)
|
32
|
+
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
|
36
|
+
yield node if block_given?
|
37
|
+
if prior_state == :ready
|
38
|
+
node.publish
|
39
|
+
mqtt.publish("#{topic}/$nodes", nodes.keys.join(","), true, 1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def remove_node(id)
|
46
|
+
return unless (node = nodes[id])
|
47
|
+
init do
|
48
|
+
node.unpublish
|
49
|
+
@nodes.delete(id)
|
50
|
+
mqtt.publish("#{topic}/$nodes", nodes.keys.join(","), true, 1) if @published
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def publish
|
55
|
+
return if @published
|
56
|
+
|
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)
|
60
|
+
|
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
|
66
|
+
|
67
|
+
unless property&.settable?
|
68
|
+
@block&.call(topic, value)
|
69
|
+
next
|
70
|
+
end
|
71
|
+
|
72
|
+
property.set(value)
|
73
|
+
end
|
74
|
+
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
|
+
|
80
|
+
@published = true
|
81
|
+
end
|
82
|
+
|
83
|
+
def disconnect
|
84
|
+
@published = false
|
85
|
+
mqtt.disconnect
|
86
|
+
@subscription_thread&.kill
|
87
|
+
end
|
88
|
+
|
89
|
+
def join
|
90
|
+
@subscription_thread&.join
|
91
|
+
end
|
92
|
+
|
93
|
+
def init
|
94
|
+
if state == :init
|
95
|
+
yield(state)
|
96
|
+
return
|
97
|
+
end
|
98
|
+
|
99
|
+
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
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def topic_regex
|
109
|
+
@topic_regex ||= Regexp.new("^#{Regexp.escape(topic)}/(?<node>#{REGEX})/(?<property>#{REGEX})/set$")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MQTT
|
4
|
+
module Homie
|
5
|
+
class Node < Base
|
6
|
+
attr_reader :device, :type, :properties
|
7
|
+
|
8
|
+
def initialize(device, id, name, type)
|
9
|
+
super(id, name)
|
10
|
+
@device = device
|
11
|
+
@type = type
|
12
|
+
@properties = {}
|
13
|
+
@published = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def topic
|
17
|
+
"#{device.topic}/#{id}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def property(*args, **kwargs, &block)
|
21
|
+
device.init do |prior_state|
|
22
|
+
property = Property.new(self, *args, **kwargs, &block)
|
23
|
+
raise ArgumentError, "Property '#{property.id}' already exists on '#{id}'" if @properties.key?(property.id)
|
24
|
+
@properties[property.id] = property
|
25
|
+
property.publish if prior_state == :ready
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove_property(id)
|
31
|
+
return unless (property = properties[id])
|
32
|
+
init do
|
33
|
+
property.unpublish
|
34
|
+
@properties.delete(id)
|
35
|
+
mqtt.publish("#{topic}/$properties", properties.keys.join(","), true, 1) if @published
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def mqtt
|
40
|
+
device.mqtt
|
41
|
+
end
|
42
|
+
|
43
|
+
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
|
48
|
+
end
|
49
|
+
|
50
|
+
mqtt.publish("#{topic}/$properties", properties.keys.join(","), true, 1)
|
51
|
+
properties.each_value(&:publish)
|
52
|
+
end
|
53
|
+
|
54
|
+
def unpublish
|
55
|
+
return unless @published
|
56
|
+
@published = false
|
57
|
+
|
58
|
+
properties.each_value(&:unpublish)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MQTT
|
4
|
+
module Homie
|
5
|
+
class Property < Base
|
6
|
+
attr_reader :node, :datatype, :format, :unit, :value
|
7
|
+
|
8
|
+
def initialize(node, id, name, datatype, value = nil, format: nil, retained: true, unit: nil, &block)
|
9
|
+
raise ArgumentError, "Invalid Homie datatype" unless %s{string integer float boolean enum color}
|
10
|
+
raise ArgumentError, "retained must be boolean" unless [true, false].include?(retained)
|
11
|
+
raise ArgumentError, "format must be nil or a string" unless format.nil? || format.is_a?(String)
|
12
|
+
raise ArgumentError, "unit must be nil or a string" unless unit.nil? || unit.is_a?(String)
|
13
|
+
super(id, name)
|
14
|
+
@node = node
|
15
|
+
@datatype = datatype
|
16
|
+
@format = format
|
17
|
+
@retained = retained
|
18
|
+
@unit = unit
|
19
|
+
@value = value
|
20
|
+
@published = false
|
21
|
+
@block = block
|
22
|
+
end
|
23
|
+
|
24
|
+
def device
|
25
|
+
node.device
|
26
|
+
end
|
27
|
+
|
28
|
+
def topic
|
29
|
+
"#{node.topic}/#{id}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def retained?
|
33
|
+
@retained
|
34
|
+
end
|
35
|
+
|
36
|
+
def settable?
|
37
|
+
!!@block
|
38
|
+
end
|
39
|
+
|
40
|
+
def value=(value)
|
41
|
+
if @value != value
|
42
|
+
@value = value
|
43
|
+
mqtt.publish(topic, value.to_s, retained?, 1) if @published
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def unit=(unit)
|
48
|
+
if unit != @unit
|
49
|
+
@unit = unit
|
50
|
+
if @published
|
51
|
+
device.init do
|
52
|
+
mqtt.publish("#{topic}/$unit", unit.to_s, true, 1)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def format=(format)
|
59
|
+
if format != @format
|
60
|
+
@format = format
|
61
|
+
if @published
|
62
|
+
device.init do
|
63
|
+
mqtt.publish("#{topic}/$format", format.to_s, true, 1)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def set(value)
|
70
|
+
@block.call(self, value)
|
71
|
+
end
|
72
|
+
|
73
|
+
def mqtt
|
74
|
+
node.mqtt
|
75
|
+
end
|
76
|
+
|
77
|
+
def publish
|
78
|
+
return if @published
|
79
|
+
|
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", "false", true, 1) unless 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
|
88
|
+
|
89
|
+
@published = true
|
90
|
+
end
|
91
|
+
|
92
|
+
def unpublish
|
93
|
+
return unless @published
|
94
|
+
@published = false
|
95
|
+
|
96
|
+
mqtt.unsubscribe("#{topic}/set") if settable?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: homie-mqtt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cody Cutrer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-12-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mqtt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.5.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.5.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '9.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '9.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
description:
|
56
|
+
email: cody@cutrer.com'
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- lib/homie-mqtt.rb
|
62
|
+
- lib/mqtt/homie/base.rb
|
63
|
+
- lib/mqtt/homie/device.rb
|
64
|
+
- lib/mqtt/homie/node.rb
|
65
|
+
- lib/mqtt/homie/property.rb
|
66
|
+
- lib/mqtt/homie/version.rb
|
67
|
+
homepage: https://github.com/ccutrer/homie-mqtt
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
metadata: {}
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.1.4
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Library for publishing devices that conform to the Homie spec.
|
90
|
+
test_files: []
|