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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rbenv-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +248 -0
- data/Rakefile +10 -0
- data/lib/losant_mqtt.rb +26 -0
- data/lib/losant_mqtt/RootCA.crt +23 -0
- data/lib/losant_mqtt/device.rb +135 -0
- data/lib/losant_mqtt/device_connection.rb +208 -0
- data/lib/losant_mqtt/utils.rb +26 -0
- data/lib/losant_mqtt/version.rb +3 -0
- data/losant_mqtt.gemspec +24 -0
- data/spec/device_spec.rb +197 -0
- data/testing.rb +35 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
|
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
# Losant Ruby MQTT Client
|
2
|
+
|
3
|
+
[](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>
|
data/Rakefile
ADDED
data/lib/losant_mqtt.rb
ADDED
@@ -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
|
data/losant_mqtt.gemspec
ADDED
@@ -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
|
data/spec/device_spec.rb
ADDED
@@ -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
|
data/testing.rb
ADDED
@@ -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
|