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.
@@ -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: []