losant_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
+ 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