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