homie-mqtt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Homie
5
+ VERSION = '1.0.0'
6
+ end
7
+ 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: []