losant_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
+ SHA1:
3
+ metadata.gz: 075c878237e2446169818a1add9b9b7497880e61
4
+ data.tar.gz: 69a11a70d3a5fa625027cbee1ca02ebe62295d25
5
+ SHA512:
6
+ metadata.gz: a04c6e4628b47c1b5c790514cb57b810696f3757586114a014f4fee5e2f7137716e7f4e93b3d19aa4dda91d158d18bfc8a32822aa2ce048f2d452fcc790194c4
7
+ data.tar.gz: 9202ed8fb8a0e83ca247059aec4c00f2dddfeab2def0c6d6d85df46da3afd1ca45dedf5e158c2b76eb61f218762f1819e7b91f73bd374469955068373d039b3d
@@ -0,0 +1,23 @@
1
+ *~
2
+ \#*
3
+ .\#*
4
+ .DS_Store
5
+ *_flymake.*
6
+ *.LCK
7
+ .bundle
8
+ vendor/bundle
9
+ bin
10
+ *.gem
11
+ *.rbc
12
+ .bundle
13
+ .config
14
+ .yardoc
15
+ _yardoc
16
+ coverage
17
+ pkg
18
+ rdoc
19
+ spec/reports
20
+ test/tmp
21
+ test/version_tmp
22
+ tmp
23
+ Gemfile.lock
@@ -0,0 +1 @@
1
+ 2.3.1
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.1
4
+ - 2.2.5
5
+ - 2.1.10
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Losant IoT, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,248 @@
1
+ # Losant Ruby MQTT Client
2
+
3
+ [![Build Status](https://travis-ci.org/Losant/losant-mqtt-ruby.svg?branch=master)](https://travis-ci.org/Losant/losant-mqtt-ruby)
4
+
5
+ The [Losant](https://www.losant.com) MQTT client provides a simple way for
6
+ custom things to communicate with the Losant platform over MQTT. You can
7
+ authenticate as a device, publish device state, and listen for device commands.
8
+
9
+ This client works with Ruby 2.1 and higher, and it depends on [Event Machine](https://github.com/eventmachine/eventmachine) to provide
10
+ event-driven I/O.
11
+
12
+ <br/>
13
+
14
+ ## Installation
15
+
16
+ The latest stable version is available in RubyGems and can be installed using
17
+
18
+ ```bash
19
+ gem install losant_mqtt
20
+ ```
21
+
22
+ <br/>
23
+
24
+ ## Example
25
+
26
+ Below is a high-level example of using the Losant Ruby MQTT client to send
27
+ the value of a temperature sensor to the Losant platform.
28
+
29
+ ```ruby
30
+ require "losant_mqtt"
31
+
32
+ EventMachine.run do
33
+
34
+ # Construct device
35
+ device = LosantMqtt::Device.new(
36
+ device_id: "my-device-id",
37
+ key: "my-app-access-key",
38
+ secret: "my-app-access-secret")
39
+
40
+ # Send temperature once every ten seconds.
41
+ EventMachine::PeriodicTimer.new(10.0) do
42
+ temp = call_out_to_your_sensor_here()
43
+ device.send_state({ temperature: temp })
44
+ puts "#{device.device_id}: Sent state"
45
+ end
46
+
47
+ # Listen for commands.
48
+ device.on(:command) do |d, command|
49
+ puts "#{d.device_id}: Command received."
50
+ puts command["name"]
51
+ puts command["payload"]
52
+ end
53
+
54
+ # Listen for connection event
55
+ device.on(:connect) do |d|
56
+ puts "#{d.device_id}: Connected"
57
+ end
58
+
59
+ # Listen for reconnection event
60
+ device.on(:reconnect) do |d|
61
+ puts "#{d.device_id}: Reconnected"
62
+ end
63
+
64
+ # Listen for disconnection event
65
+ device.on(:close) do |d, reason|
66
+ puts "#{d.device_id}: Lost connection (#{reason})"
67
+ end
68
+
69
+ # Connect to Losant.
70
+ device.connect
71
+ end
72
+ ```
73
+
74
+ <br/>
75
+
76
+ ## API Documentation
77
+
78
+ * [Device](#device)
79
+ * [initializer](#initializer)
80
+ * [connect](#connect)
81
+ * [connected?](#connected)
82
+ * [close](#close)
83
+ * [send_state](#send_state)
84
+ * [on](#on)
85
+ * [add_listener](#add_listener)
86
+ * [remove_listener](#remove_listener)
87
+
88
+ ### Device
89
+
90
+ A device represents a single thing or widget that you'd like to connect to
91
+ the Losant platform. A single device can contain many different sensors or
92
+ other attached peripherals. Devices can either report state or
93
+ respond to commands.
94
+
95
+ A device's state represents a snapshot of the device at some point in time.
96
+ If the device has a temperature sensor, it might report state every few seconds
97
+ with the temperature. If a device has a button, it might only report state when
98
+ the button is pressed. Devices can report state as often as needed by your
99
+ specific application.
100
+
101
+ Commands instruct a device to take a specific action. Commands are defined as a
102
+ name and an optional payload. For example, if the device is a scrolling marquee,
103
+ the command might be "update text" and the payload would include the text
104
+ to update.
105
+
106
+ #### initializer
107
+
108
+ ```ruby
109
+ LosantMqtt::Device.new(device_id:, key:, secret:, secure: true, retry_lost_connection: true)
110
+ ```
111
+
112
+ The ``Client()`` initializer takes the following arguments:
113
+
114
+ * device_id
115
+ The device's ID. Obtained by first registering a device using
116
+ the Losant platform.
117
+
118
+ * key
119
+ The Losant access key.
120
+
121
+ * secret
122
+ The Losant access secret.
123
+
124
+ * secure
125
+ If the client should connect to Losant over SSL - default is true.
126
+
127
+ * retry_lost_connection
128
+ If the client should retry lost connections - default is true. Errors on
129
+ initial connect will still be raised, but if a good connection is
130
+ subsequently lost and this flag is true, the client will try to automatically
131
+ reconnect and will not raise errors (except in the case of authentication
132
+ errors, which will still be raised). When this flag is true, disconnection
133
+ and reconnection can be monitored using the `:close` and `:reconnect` events.
134
+
135
+ ###### Example
136
+
137
+ ```ruby
138
+ device = LosantMqtt::Device.new(device_id: "my-device-id",
139
+ key: "my-app-access-key", secret: "my-app-access-secret")
140
+ ```
141
+
142
+ #### connect
143
+
144
+ ```ruby
145
+ connect()
146
+ ```
147
+
148
+ Connects the device to the Losant platform. Hook the `:connect` event to know when
149
+ a connection has been successfully established. Returns the device instance
150
+ to allow chaining.
151
+
152
+ #### connected?
153
+
154
+ ```ruby
155
+ connected?()
156
+ ```
157
+
158
+ Returns a boolean indicating whether or not the device is currently connected
159
+ to the Losant platform.
160
+
161
+ #### close
162
+
163
+ ```ruby
164
+ close()
165
+ ```
166
+
167
+ Closes the device's connection to the Losant platform.
168
+
169
+ #### send_state
170
+
171
+ ```ruby
172
+ send_state(state, time = nil)
173
+ ```
174
+
175
+ Sends a device state to the Losant platform. In many scenarios, device
176
+ states will change rapidly. For example a GPS device will report GPS
177
+ coordinates once a second or more. Because of this, sendState is typically
178
+ the most invoked function. Any state data sent to Losant is stored and made
179
+ available in data visualization tools and workflow triggers.
180
+
181
+ * state
182
+ The state to send as a hash.
183
+
184
+ * time
185
+ When the state occured - if nil or not set, will default to now.
186
+
187
+ ###### Example
188
+
189
+ ```ruby
190
+ device.send_state({ voltage: read_analog_in() })
191
+ ```
192
+
193
+ #### on
194
+
195
+ ```ruby
196
+ on(event, proc=nil, &block)
197
+ ```
198
+
199
+ Adds an observer to listen for an event on this device.
200
+
201
+ * event
202
+ The event name to listen for. Possible events are: `:connect` (the device
203
+ has connected), `:reconnect` (the device lost its connection and reconnected),
204
+ `:close` (the device's connection was closed), and
205
+ `:command` (the device has received a command from Losant).
206
+
207
+ * proc / &block
208
+ The proc or block to call with the given event fires. The first
209
+ argument for all callbacks will be the device instance. For `:close` callbacks,
210
+ there can be a second argument which is the reason for the closing of the
211
+ connection, and for `:command` callbacks the second argument is the command
212
+ received.
213
+
214
+ ###### Example
215
+
216
+ ```ruby
217
+ device.on(:command) do |device, command|
218
+ puts "Command received."
219
+ puts command["name"]
220
+ puts command["payload"]
221
+ end
222
+ ```
223
+
224
+ #### add_listener
225
+
226
+ An alias to [on](#on).
227
+
228
+ #### remove_listener
229
+
230
+ ```ruby
231
+ remove_listener(event, proc)
232
+ ```
233
+
234
+ Removes an observer from listening for an event on this device.
235
+
236
+ * event
237
+ The event name to stop listening for. Same events as [on](#on).
238
+
239
+ * proc
240
+ The proc that should be removed.
241
+
242
+ <br/>
243
+
244
+ *****
245
+
246
+ Copyright (c) 2016 Losant IoT, Inc
247
+
248
+ <https://www.losant.com>
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require "bundler/gem_tasks"
5
+ require "rspec/core/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => :spec
10
+ task :test => :spec
@@ -0,0 +1,26 @@
1
+ require "openssl"
2
+ require "date"
3
+ require "json"
4
+
5
+ require "mqtt"
6
+ require "eventmachine"
7
+ require "events"
8
+
9
+ require_relative "losant_mqtt/version"
10
+ require_relative "losant_mqtt/utils"
11
+ require_relative "losant_mqtt/device_connection"
12
+ require_relative "losant_mqtt/device"
13
+
14
+ module LosantMqtt
15
+ COMMAND_TOPIC = "losant/%{device_id}/command"
16
+ STATE_TOPIC = "losant/%{device_id}/state"
17
+ DEFAULT_ENDPOINT = "broker.losant.com"
18
+
19
+ def self.endpoint
20
+ @endpoint || DEFAULT_ENDPOINT
21
+ end
22
+
23
+ def self.endpoint=(domain)
24
+ @endpoint = domain
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
3
+ MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
4
+ d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
5
+ ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
6
+ MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
7
+ LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
8
+ RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
9
+ +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
10
+ PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
11
+ xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
12
+ Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
13
+ hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
14
+ EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
15
+ MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
16
+ FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
17
+ nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
18
+ eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
19
+ hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
20
+ Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
21
+ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
22
+ +OkuE6N36B9K
23
+ -----END CERTIFICATE-----
@@ -0,0 +1,135 @@
1
+ module LosantMqtt
2
+ class Device
3
+ include Events::Emitter
4
+
5
+ attr_reader :device_id, :key, :secret, :secure, :should_retry
6
+
7
+ def initialize(options = {})
8
+ @was_connected = false
9
+
10
+ @device_id = options[:device_id].to_s
11
+ @key = options[:key].to_s
12
+ @secret = options[:secret].to_s
13
+ @secure = options.has_key?(:secure) ? !!options[:secure] : true
14
+ @should_retry = options.has_key?(:retry_lost_connection) ?
15
+ !!options[:retry_lost_connection] : true
16
+
17
+ raise ArgumentError.new("Invalid Device Id") if @device_id == ""
18
+ raise ArgumentError.new("Invalid Key") if @key == ""
19
+ raise ArgumentError.new("Invalid Secret") if @secret == ""
20
+ end
21
+
22
+ def connected?
23
+ !!(@connection && @connection.connected?)
24
+ end
25
+
26
+ def connect
27
+ return self if @retry_timer || @connection
28
+
29
+ begin
30
+ @connection = DeviceConnection.connect(
31
+ host: LosantMqtt.endpoint,
32
+ port: @secure ? 8883 : 1883,
33
+ secure: @secure,
34
+ username: @key,
35
+ password: @secret,
36
+ client_id: @device_id)
37
+ rescue Exception => ex
38
+ if @was_connected && @should_retry
39
+ @connection = nil
40
+ emit(:close, self, ex)
41
+ retry_lost_connection
42
+ return self
43
+ else
44
+ raise ex
45
+ end
46
+ end
47
+
48
+ @connection.on(:disconnected) do |reason|
49
+ @connection = nil
50
+ emit(:close, self, reason)
51
+
52
+ if reason
53
+ if @was_connected && @should_retry && !(reason.message =~ /Authentication Error/)
54
+ # if it was not an authentication error
55
+ # and we ave successfully connected before
56
+ # attempt to reconnect in a few seconds
57
+ retry_lost_connection
58
+ else
59
+ raise reason
60
+ end
61
+ end
62
+ end
63
+
64
+ @connection.on(:connected) do
65
+ if(@state_backlog)
66
+ @connection.publish(state_topic, @state_backlog.to_json)
67
+ @state_backlog = nil
68
+ end
69
+
70
+ if @was_connected
71
+ emit(:reconnect, self)
72
+ else
73
+ @was_connected = true
74
+ emit(:connect, self)
75
+ end
76
+
77
+ @connection.subscribe(command_topic) do |msg|
78
+ begin
79
+ msg = Utils.convert_ext_json(JSON.parse(msg))
80
+ rescue JSON::ParserError
81
+ msg = nil
82
+ end
83
+ emit(:command, self, msg) if msg
84
+ end
85
+ end
86
+
87
+ self
88
+ end
89
+
90
+ def close
91
+ @connection.disconnect if @connection
92
+ if @retry_timer
93
+ @retry_timer.cancel
94
+ @retry_timer = nil
95
+ end
96
+ true
97
+ end
98
+
99
+ def send_state(state, time = nil)
100
+ connect unless @connection
101
+
102
+ time ||= Time.now
103
+ time = time.to_time if time.respond_to?(:to_time)
104
+ time = time.to_f
105
+ time = time * 1000 if time < 1000000000000 # convert to ms since epoch
106
+ time = time.round
107
+
108
+ payload = { time: time, data: state }
109
+
110
+ if connected?
111
+ @connection.publish(state_topic, payload.to_json)
112
+ else
113
+ (@state_backlog ||= []).push(payload)
114
+ end
115
+
116
+ true
117
+ end
118
+
119
+ def retry_lost_connection(wait=5)
120
+ return false if @connection
121
+ @retry_timer ||= EventMachine::Timer.new(wait) do
122
+ @retry_timer = nil
123
+ connect
124
+ end
125
+ end
126
+
127
+ def state_topic
128
+ @state_topic ||= LosantMqtt::STATE_TOPIC % { device_id: @device_id }
129
+ end
130
+
131
+ def command_topic
132
+ @command_topic ||= LosantMqtt::COMMAND_TOPIC % { device_id: @device_id }
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,208 @@
1
+ module LosantMqtt
2
+ CA_FILE_PATH = File.expand_path(File.join(File.dirname(__FILE__), "RootCA.crt"))
3
+
4
+ class DeviceConnection < EventMachine::Connection
5
+ include Events::Emitter
6
+
7
+ attr_reader :state
8
+
9
+ def self.connect(options = {})
10
+ options = { host: "localhost", port: MQTT::DEFAULT_PORT }.merge(options)
11
+ EventMachine.connect(options[:host], options[:port], self, options)
12
+ end
13
+
14
+ def initialize(options = {})
15
+ @options = {
16
+ client_id: MQTT::Client.generate_client_id,
17
+ keep_alive: 15,
18
+ clean_session: true,
19
+ username: nil,
20
+ password: nil,
21
+ version: "3.1.1"
22
+ }.merge(options)
23
+ @subscriptions = {}
24
+ end
25
+
26
+ def connected?
27
+ state == :connected
28
+ end
29
+
30
+ def publish(topic, payload)
31
+ send_packet(MQTT::Packet::Publish.new(
32
+ id: next_packet_id,
33
+ qos: 0,
34
+ retain: false,
35
+ topic: topic,
36
+ payload: payload))
37
+ end
38
+
39
+ def subscribe(topic, &block)
40
+ @subscriptions[topic] = block
41
+ send_packet(MQTT::Packet::Subscribe.new(id: next_packet_id, topics: [topic]))
42
+ end
43
+
44
+ def unsubscribe(topic)
45
+ @subscriptions.delete(topic)
46
+ send_packet(MQTT::Packet::Unsubscribe.new(id: next_packet_id, topics: [topic]))
47
+ end
48
+
49
+ def disconnect(send_msg: true)
50
+ return if @state == :disconnecting || @state == :disconnected
51
+ packet = connected? && send_msg && MQTT::Packet::Disconnect.new
52
+ @state = :disconnecting
53
+ emit(@state)
54
+ packet ? send_packet(packet) : close_connection
55
+ end
56
+
57
+ def send_connect_packet
58
+ packet = MQTT::Packet::Connect.new(
59
+ client_id: @options[:client_id],
60
+ clean_session: @options[:clean_session],
61
+ keep_alive: @options[:keep_alive],
62
+ username: @options[:username],
63
+ password: @options[:password],
64
+ version: @options[:version])
65
+ send_packet(packet)
66
+ @state = :connect_sent
67
+ emit(@state)
68
+ end
69
+
70
+ def process_packet(packet)
71
+ @last_received = Time.now.to_i
72
+ if state == :connect_sent && packet.class == MQTT::Packet::Connack
73
+ connect_ack(packet)
74
+ elsif state == :connected && packet.class == MQTT::Packet::Pingresp
75
+ # Pong!
76
+ elsif state == :connected && packet.class == MQTT::Packet::Publish
77
+ @subscriptions[packet.topic].call(packet.payload) if @subscriptions[packet.topic]
78
+ elsif state == :connected && packet.class == MQTT::Packet::Puback
79
+ # publish acked
80
+ elsif state == :connected && packet.class == MQTT::Packet::Suback
81
+ # Subscribed!
82
+ elsif state == :connected && packet.class == MQTT::Packet::Unsuback
83
+ # Unsubscribed!
84
+ else
85
+ # CONNECT only sent by client
86
+ # SUBSCRIBE only sent by client
87
+ # PINGREQ only sent by client
88
+ # UNSUBSCRIBE only sent by client
89
+ # DISCONNECT only sent by client
90
+ # PUBREC/PUBREL/PUBCOMP for QOS2 - do not support
91
+ @ex = MQTT::ProtocolException.new("Wasn't expecting packet of type #{packet.class} when in state #{state}")
92
+ close_connection
93
+ end
94
+ end
95
+
96
+ def connect_ack(packet)
97
+ if packet.return_code != 0x00
98
+ @ex = MQTT::ProtocolException.new("Authentication Error - " + packet.return_msg)
99
+ return close_connection
100
+ end
101
+
102
+ @state = :connected
103
+
104
+ if @options[:keep_alive] > 0
105
+ @timer = EventMachine::PeriodicTimer.new(@options[:keep_alive]) do
106
+ if(Time.now.to_i - @last_received > @options[:keep_alive])
107
+ @ex = MQTT::NotConnectedException.new("Keep alive failure, disconnecting")
108
+ close_connection
109
+ else
110
+ send_packet(MQTT::Packet::Pingreq.new)
111
+ end
112
+ end
113
+ end
114
+
115
+ emit(@state)
116
+ end
117
+
118
+ def next_packet_id
119
+ @packet_id += 1
120
+ end
121
+
122
+ def send_packet(packet)
123
+ send_data(packet.to_s)
124
+ end
125
+
126
+ def unbind(msg)
127
+ if @timer
128
+ @timer.cancel
129
+ @timer = nil
130
+ end
131
+
132
+ unless @state == :disconnecting
133
+ @ex ||= $! || MQTT::NotConnectedException.new("Connection to server lost")
134
+ end
135
+
136
+ @state = :disconnected
137
+ emit(@state, @ex)
138
+ @ex = nil
139
+ end
140
+
141
+ def receive_data(data)
142
+ @data << data
143
+
144
+ # Are we at the start of a new packet?
145
+ if !@packet && @data.length >= 2
146
+ @packet = MQTT::Packet.parse_header(@data)
147
+ end
148
+
149
+ # Do we have the the full packet body now?
150
+ if @packet && @data.length >= @packet.body_length
151
+ @packet.parse_body(@data.slice!(0...@packet.body_length))
152
+ process_packet(@packet)
153
+ @packet = nil
154
+ receive_data("")
155
+ end
156
+ end
157
+
158
+ def post_init
159
+ @state = :connecting
160
+ @last_received = 0
161
+ @packet_id = 0
162
+ @packet = nil
163
+ @data = ""
164
+ @ex = nil
165
+ emit(@state)
166
+ end
167
+
168
+ def connection_completed
169
+ if @options[:secure]
170
+ @last_seen_cert = nil
171
+ @certificate_store = OpenSSL::X509::Store.new
172
+ @certificate_store.add_file(CA_FILE_PATH)
173
+ start_tls(:verify_peer => true)
174
+ else
175
+ send_connect_packet
176
+ end
177
+ end
178
+
179
+ def ssl_verify_peer(cert_string)
180
+ @last_seen_cert = OpenSSL::X509::Certificate.new(cert_string)
181
+ unless @certificate_store.verify(@last_seen_cert)
182
+ @ex = OpenSSL::OpenSSLError.new("Unable to verify the certificate for #{@options[:host]}")
183
+ return false
184
+ end
185
+
186
+ begin
187
+ @certificate_store.add_cert(@last_seen_cert)
188
+ rescue OpenSSL::X509::StoreError => e
189
+ unless e.message == "cert already in hash table"
190
+ @ex = e
191
+ return false
192
+ end
193
+ end
194
+
195
+ true
196
+ end
197
+
198
+ def ssl_handshake_completed
199
+ unless OpenSSL::SSL.verify_certificate_identity(@last_seen_cert, @options[:host])
200
+ @ex = OpenSSL::OpenSSLError.new("The hostname #{@options[:host]} does not match the server certificate")
201
+ return close_connection
202
+ end
203
+
204
+ send_connect_packet
205
+ end
206
+
207
+ end
208
+ end
@@ -0,0 +1,26 @@
1
+ module LosantMqtt
2
+ module Utils
3
+
4
+ def self.convert_ext_json(value)
5
+ if value.respond_to?(:to_ary)
6
+ value = value.to_ary.map{ |v| convert_ext_json(v) }
7
+ end
8
+
9
+ if value.respond_to?(:to_hash)
10
+ value = value.to_hash
11
+ if value.length == 1 && value.has_key?("$date")
12
+ value = ::DateTime.parse(value["$date"])
13
+ elsif value.length == 1 && value.has_key?("$undefined")
14
+ value = nil
15
+ else
16
+ value.each do |k, v|
17
+ value[k] = convert_ext_json(v)
18
+ end
19
+ end
20
+ end
21
+
22
+ value
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module LosantMqtt
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.push(File.expand_path("../lib", __FILE__))
2
+ require "losant_mqtt/version"
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "losant_mqtt"
6
+ gem.authors = ["Michael Kuehl"]
7
+ gem.email = ["hello@losant.com"]
8
+ gem.summary = %q{An MQTT client for the Losant MQTT Broker}
9
+ gem.description = %q{Easily use the Losant IoT Platform through its MQTT Broker with Ruby}
10
+ gem.homepage = "https://www.losant.com"
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.require_paths = ["lib"]
15
+ gem.version = LosantMqtt::VERSION
16
+ gem.licenses = ["MIT"]
17
+
18
+ gem.add_runtime_dependency "eventmachine", "~> 1.2.0"
19
+ gem.add_runtime_dependency "mqtt", "~> 0.3.0"
20
+ gem.add_runtime_dependency "events", "~> 0.9.0"
21
+
22
+ gem.add_development_dependency "rspec", "~> 3.0"
23
+ gem.add_development_dependency "rake", "~> 11"
24
+ end
@@ -0,0 +1,197 @@
1
+ require "losant_mqtt"
2
+
3
+ describe LosantMqtt::Device do
4
+
5
+ it "raises error on missing required initializer args" do
6
+ expect do
7
+ LosantMqtt::Device.new(key: "key", secret: "secret")
8
+ end.to raise_error(ArgumentError)
9
+
10
+ expect do
11
+ LosantMqtt::Device.new(device_id: "device_id", secret: "secret")
12
+ end.to raise_error(ArgumentError)
13
+
14
+ expect do
15
+ LosantMqtt::Device.new(device_id: "device_id", key: "key")
16
+ end.to raise_error(ArgumentError)
17
+ end
18
+
19
+ it "should correctly connect, send state, and receive a command" do
20
+ EventMachine.run do
21
+ EventMachine.add_timer(5) { raise RuntimeError.new("Test Timed Out") }
22
+
23
+ # associated with app id 57615eebc035bd0100cb964a
24
+ # workflow that takes state reported and sends a command back
25
+ device = LosantMqtt::Device.new(
26
+ device_id: ENV["DEVICE_ID"] || "57615f0ac035bd0100cb964b",
27
+ key: ENV["LOSANT_KEY"] || "0bbdc673-77ea-423f-9241-976db4776f8b",
28
+ secret: ENV["LOSANT_SECRET"])
29
+ expect(device.connected?).to eq(false)
30
+
31
+ callbacks_called = []
32
+
33
+ device.on(:command) do |d, cmd|
34
+ expect(d.connected?).to eq(true)
35
+ callbacks_called.push(:command)
36
+ expect(cmd["name"]).to eq("triggeredCommand")
37
+ expect(cmd["payload"]).to eq({ "result" => "one-1-false" })
38
+ d.close
39
+ end
40
+
41
+ device.on(:connect) do |d|
42
+ expect(d.connected?).to eq(true)
43
+ callbacks_called.push(:connect)
44
+ end
45
+
46
+ device.on(:reconnect) do
47
+ callbacks_called.push(:reconnect)
48
+ end
49
+
50
+ device.on(:close) do |d, reason|
51
+ expect(d.connected?).to eq(false)
52
+ expect(reason).to eq(nil)
53
+ expect(callbacks_called).to eq([:connect, :command])
54
+ EventMachine.stop_event_loop
55
+ end
56
+
57
+ device.send_state({ str_attr: "one", num_attr: 1, bool_attr: false })
58
+ end
59
+ end
60
+
61
+ it "should reconnect when connection is abnormally lost and flag is true" do
62
+ EventMachine.run do
63
+ EventMachine.add_timer(10) { raise RuntimeError.new("Test Timed Out") }
64
+
65
+ # associated with app id 57615eebc035bd0100cb964a
66
+ # workflow that takes state reported and sends a command back
67
+ device = LosantMqtt::Device.new(
68
+ device_id: ENV["DEVICE_ID"] || "57615f0ac035bd0100cb964b",
69
+ key: ENV["LOSANT_KEY"] || "0bbdc673-77ea-423f-9241-976db4776f8b",
70
+ secret: ENV["LOSANT_SECRET"])
71
+ expect(device.connected?).to eq(false)
72
+
73
+ callbacks_called = []
74
+
75
+ device.on(:command) do |d, cmd|
76
+ expect(d.connected?).to eq(true)
77
+ callbacks_called.push(:command)
78
+ expect(cmd["name"]).to eq("triggeredCommand")
79
+ expect(cmd["payload"]).to eq({ "result" => "two-2-true" })
80
+ d.close
81
+ end
82
+
83
+ device.on(:connect) do |d|
84
+ expect(d.connected?).to eq(true)
85
+ callbacks_called.push(:connect)
86
+ EventMachine.add_timer(0.1) do
87
+ # abnormally force close underlying socket
88
+ d.instance_variable_get("@connection").close_connection
89
+ end
90
+ end
91
+
92
+ device.on(:reconnect) do |d|
93
+ expect(d.connected?).to eq(true)
94
+ callbacks_called.push(:reconnect)
95
+ d.send_state({ str_attr: "two", num_attr: 2, bool_attr: true })
96
+ end
97
+
98
+ close_count = 0
99
+ device.on(:close) do |d, reason|
100
+ expect(d.connected?).to eq(false)
101
+ callbacks_called.push(:close)
102
+ close_count += 1
103
+ if close_count == 1
104
+ expect(callbacks_called).to eq([:connect, :close])
105
+ expect(reason.message).to eq("Connection to server lost")
106
+ else
107
+ expect(reason).to eq(nil)
108
+ expect(callbacks_called).to eq([:connect, :close, :reconnect, :command, :close])
109
+ EventMachine.stop_event_loop
110
+ end
111
+ end
112
+
113
+ device.connect
114
+ end
115
+ end
116
+
117
+ it "should not reconnect when connection is abnormally lost and flag is false" do
118
+ expect do
119
+ EventMachine.run do
120
+ EventMachine.add_timer(10) { raise RuntimeError.new("Test Timed Out") }
121
+
122
+ # associated with app id 57615eebc035bd0100cb964a
123
+ # workflow that takes state reported and sends a command back
124
+ device = LosantMqtt::Device.new(
125
+ device_id: ENV["DEVICE_ID"] || "57615f0ac035bd0100cb964b",
126
+ key: ENV["LOSANT_KEY"] || "0bbdc673-77ea-423f-9241-976db4776f8b",
127
+ secret: ENV["LOSANT_SECRET"],
128
+ retry_lost_connection: false)
129
+ expect(device.connected?).to eq(false)
130
+
131
+ callbacks_called = []
132
+
133
+ device.on(:reconnect) do
134
+ callbacks_called.push(:reconnect)
135
+ end
136
+
137
+ device.on(:command) do
138
+ callbacks_called.push(:command)
139
+ end
140
+
141
+ device.on(:connect) do |d|
142
+ expect(d.connected?).to eq(true)
143
+ callbacks_called.push(:connect)
144
+ EventMachine.add_timer(0.1) do
145
+ # abnormally force close underlying socket
146
+ d.instance_variable_get("@connection").close_connection
147
+ end
148
+ end
149
+
150
+ device.on(:close) do |d, reason|
151
+ expect(d.connected?).to eq(false)
152
+ callbacks_called.push(:close)
153
+ expect(callbacks_called).to eq([:connect, :close])
154
+ expect(reason.message).to eq("Connection to server lost")
155
+ end
156
+
157
+ device.connect
158
+ end
159
+ end.to raise_error(MQTT::NotConnectedException)
160
+ end
161
+
162
+ it "should raise errors on initial bad connect" do
163
+ expect do
164
+ EventMachine.run do
165
+ EventMachine.add_timer(10) { raise RuntimeError.new("Test Timed Out") }
166
+ device = LosantMqtt::Device.new(
167
+ device_id: "not a device id",
168
+ key: "not a key",
169
+ secret: "not a secret")
170
+
171
+ callbacks_called = []
172
+
173
+ device.on(:reconnect) do
174
+ callbacks_called.push(:reconnect)
175
+ end
176
+
177
+ device.on(:command) do
178
+ callbacks_called.push(:command)
179
+ end
180
+
181
+ device.on(:connect) do
182
+ callbacks_called.push(:connect)
183
+ end
184
+
185
+ device.on(:close) do |d, reason|
186
+ expect(d.connected?).to eq(false)
187
+ callbacks_called.push(:close)
188
+ expect(callbacks_called).to eq([:close])
189
+ expect(reason.message).to eq("Authentication Error - Connection refused: not authorised")
190
+ end
191
+
192
+ device.connect
193
+ end
194
+ end.to raise_error(MQTT::ProtocolException)
195
+ end
196
+
197
+ end
@@ -0,0 +1,35 @@
1
+ $LOAD_PATH.push(File.expand_path("../lib", __FILE__));
2
+ require "losant_mqtt"
3
+
4
+ EventMachine.run do
5
+ device = LosantMqtt::Device.new(
6
+ key: "0bbdc673-77ea-423f-9241-976db4776f8b",
7
+ secret: "191bcd2c2512a770a880460f04f46ccf295c414a44b41742c9f3fe0746245fa8",
8
+ device_id: "57615f0ac035bd0100cb964b")
9
+
10
+ EventMachine::PeriodicTimer.new(10.0) do
11
+ temp = 10
12
+ device.send_state({ temperature: temp })
13
+ puts "#{device.device_id}: Sent state"
14
+ end
15
+
16
+ device.on(:command) do |d, command|
17
+ puts "#{d.device_id}: Command received."
18
+ puts command["name"]
19
+ puts command["payload"]
20
+ end
21
+
22
+ device.on(:connect) do |d|
23
+ puts "#{d.device_id}: Connected"
24
+ end
25
+
26
+ device.on(:reconnect) do |d|
27
+ puts "#{d.device_id}: Reconnected"
28
+ end
29
+
30
+ device.on(:close) do |d, reason|
31
+ puts "#{d.device_id}: Lost connection (#{reason})"
32
+ end
33
+
34
+ device.connect
35
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: losant_mqtt
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Kuehl
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eventmachine
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: mqtt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.3.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: events
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.9.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.9.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '11'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '11'
83
+ description: Easily use the Losant IoT Platform through its MQTT Broker with Ruby
84
+ email:
85
+ - hello@losant.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rbenv-version"
92
+ - ".travis.yml"
93
+ - Gemfile
94
+ - LICENSE
95
+ - README.md
96
+ - Rakefile
97
+ - lib/losant_mqtt.rb
98
+ - lib/losant_mqtt/RootCA.crt
99
+ - lib/losant_mqtt/device.rb
100
+ - lib/losant_mqtt/device_connection.rb
101
+ - lib/losant_mqtt/utils.rb
102
+ - lib/losant_mqtt/version.rb
103
+ - losant_mqtt.gemspec
104
+ - spec/device_spec.rb
105
+ - testing.rb
106
+ homepage: https://www.losant.com
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.6.4
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: An MQTT client for the Losant MQTT Broker
130
+ test_files:
131
+ - spec/device_spec.rb