dropcam 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ .DS_Store
2
+ *.flv
3
+
4
+ *.gem
5
+ *.rbc
6
+ .bundle
7
+ .config
8
+ .yardoc
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'awesome_print'
4
+ # Specify your gem's dependencies in dropcam.gemspec
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Nolan Brown
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # Dropcam
2
+
3
+ RubyGem to access Dropcam account and Camera including direct live stream access
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'dropcam'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install dropcam
18
+
19
+ ## Usage
20
+
21
+ require 'dropcam'
22
+
23
+ dropcam = Dropcam::Dropcam.new("<USERNAME>","<PASSWORD>")
24
+ camera = dropcam.cameras.first
25
+
26
+ # returns jpg image data of the latest frame captured
27
+ screenshot = camera.current_image
28
+
29
+ # write data to disk
30
+ File.open("#{camera.title}.jpg", 'w') {|f| f.write(screenshot) }
31
+
32
+ # access and modify settings
33
+ # this disables the watermark on your camera stream
34
+ settings = camera.settings
35
+ settings["watermark.enabled"].set(false)
36
+
37
+
38
+ ## Live Stream
39
+
40
+ **Streaming isn't directly integrated currently and it's up to you to find a player. Some of the players available:**
41
+
42
+ - VLC (RTSP/RTMP)
43
+ - RTMPDump (RTMP)
44
+ - openRTSP (RTSP)
45
+
46
+ The easiest way to record the live camera stream is with RTMPDump. Install via homebrew:
47
+
48
+ `$ brew install rtmpdump`
49
+
50
+ To save a live stream:
51
+
52
+ require 'dropcam'
53
+ dropcam = Dropcam::Dropcam.new("<USERNAME>","<PASSWORD>")
54
+ camera = dropcam.cameras.first
55
+
56
+ # record the live stream for 30 seconds
57
+ camera.stream.save_live("#{camera.title}.flv", 30)
58
+
59
+ # to get access information to use a third party application
60
+ # RTMP/Flash Streaming
61
+ camera.stream.rtmp_details
62
+
63
+ # RTSP Streaming
64
+ camera.stream.rtsp_details
65
+
66
+
67
+ Currently stream resolution is limited to 400x240.
68
+
69
+
70
+
71
+ ## NOTES: ##
72
+
73
+ The Dropcam API is unofficial and unreleased. This code can break at anytime as Dropcam changes/updates their service.
74
+
75
+ This gem has only been tested on Mac OS 10.8 running Ruby 1.9.3
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/dropcam.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dropcam/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "dropcam"
8
+ gem.version = Dropcam::VERSION
9
+ gem.authors = ["Nolan Brown"]
10
+ gem.email = ["nolanbrown@gmail.com"]
11
+ gem.description = %q{Access Dropcam account and cameras}
12
+ gem.summary = gem.description
13
+ gem.homepage = "https://github.com/nolanbrown/dropcam"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+
21
+ end
data/example/basics.rb ADDED
@@ -0,0 +1,15 @@
1
+ #dropcam = Dropcam::Session.new("username","password")
2
+ lib = File.expand_path('../../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'dropcam'
6
+ require 'awesome_print'
7
+ dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
8
+ camera = dropcam.cameras.first
9
+ ap camera.uuid
10
+ ap camera.session_token
11
+ ap camera.rtsp_url
12
+ ap camera.rtmpdump_stream_command
13
+
14
+ settings = camera.settings
15
+ #ap settings["watermark.enabled"].set(false)
data/example/camera.rb ADDED
@@ -0,0 +1,16 @@
1
+ lib = File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'dropcam'
5
+ require 'awesome_print'
6
+
7
+ dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
8
+
9
+ all_cameras = dropcam.cameras
10
+ all_cameras.each { |aCamera|
11
+ puts "\n------------------------------------\n"
12
+ aCamera.instance_variables.each{|variable|
13
+ print "#{variable} : "; $stdout.flush
14
+ ap aCamera.instance_variable_get(variable)
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ lib = File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'dropcam'
5
+ require 'awesome_print'
6
+ dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
7
+ camera = dropcam.cameras.first
8
+ camera.notification_devices.each{|notification|
9
+ # print all variable values
10
+ notification.instance_variables.each{|variable|
11
+ print "#{variable} : "; $stdout.flush
12
+ ap notification.instance_variable_get(variable)
13
+ }
14
+
15
+ # enable or disable notification
16
+ # notification.set_enabled(true)
17
+ puts "\n------------------------------------\n\n"
18
+
19
+ }
@@ -0,0 +1,16 @@
1
+ lib = File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'dropcam'
5
+ require 'awesome_print'
6
+ dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
7
+ camera = dropcam.cameras.first
8
+ settings = camera.settings
9
+
10
+ ap settings
11
+
12
+ setting_name = "watermark.enabled"
13
+ setting_value = settings[setting_name].value
14
+ puts "Changing #{setting_name} from #{setting_value} to #{!setting_value}"
15
+ settings[setting_name].set(!setting_value)
16
+ puts "Changing #{setting_name} is now #{settings[setting_name].value}"
data/example/stream.rb ADDED
@@ -0,0 +1,16 @@
1
+ lib = File.expand_path('../../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'dropcam'
5
+
6
+ dropcam = Dropcam::Dropcam.new(ENV["DROPCAM_USERNAME"],ENV["DROPCAM_PASSWORD"])
7
+ camera = dropcam.cameras.first
8
+
9
+ puts "\nRTSP URI"
10
+ puts camera.stream.rtsp_uri.to_s
11
+
12
+ puts "\nRTMPDump command"
13
+ puts camera.stream.rtmpdump
14
+
15
+ puts "\nSave Live Stream as FLV"
16
+ puts camera.stream.save_live("#{camera.title}.flv",30)
@@ -0,0 +1,126 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module Net
6
+ class HTTPResponse
7
+ def success?
8
+ self.code.to_i == 200
9
+ end
10
+ def not_found?
11
+ self.code.to_i == 404
12
+ end
13
+ def error?
14
+ self.code.to_i == 400
15
+ end
16
+ def not_authorized?
17
+ self.code.to_i == 403
18
+ end
19
+ end
20
+ end
21
+
22
+ module Dropcam
23
+ class Base
24
+ attr_accessor :session_token, :cookies
25
+
26
+
27
+ ::NEXUS_API_BASE = "https://nexusapi.dropcam.com/"
28
+
29
+ ::NEXUS_GET_IMAGE_PATH = "get_image" # uuid and width
30
+ ::NEXUS_GET_AVAILABLE_PATH = "get_available" # start_time and uuid
31
+ ::NEXUS_GET_CUEPOINT_PATH = "get_cuepoint" # start_time_uuid
32
+ ::NEXUS_GET_EVENT_CLIP_PATH = "get_event_clip" # start_time_uuid
33
+ ::NEXUS_GET_REVERSE_PAGINATED_CUEPOINTS_PATH = "get_reverse_paginated_cuepoint"
34
+ ::API_BASE = "https://www.dropcam.com"
35
+ ::API_PATH = "/api/v1"
36
+
37
+
38
+ ::CAMERA_HTML_SETTINGS_BASE = "/cameras/settings/" # /uuid
39
+
40
+ ::USERS_LOGIN = "#{API_PATH}/login.login"
41
+ ::CAMERAS_UPDATE = "#{API_PATH}/cameras.update" # uuid, is_public
42
+ ::CAMERAS_GET_BY_PUBLIC_TOKEN = "#{API_PATH}/cameras.get_by_public_token"
43
+ ::CAMERAS_GET = "#{API_PATH}/cameras.get" # uuid
44
+ ::CAMERAS_GET_VISIBLE = "#{API_PATH}/cameras.get_visible"
45
+ ::CAMERAS_GET_PUBLIC = "#{API_PATH}/cameras.get_demo"
46
+
47
+ ::DROPCAMS_GET_PROPERTIES = "#{API_PATH}/dropcams.get_properties" # uuid
48
+ ::DROPCAMS_SET_PROPERTY = "#{API_PATH}/dropcams.set_property" # POST: uuid, key, value
49
+ ::CAMERA_NOTIFICATION_UPDATE = "#{API_PATH}/camera_notifications.update"
50
+ ::CAMERA_ADD_EMAIL_NOTIFICATION = "#{API_PATH}/users.add_email_notification_target"
51
+ ::CAMERA_DELETE_NOTIFICATION = "#{API_PATH}/users.delete_notification_target"
52
+ ::CAMERA_FIND_NOTIFICATIONS = "#{API_PATH}/camera_notifications.find_by_camera"
53
+
54
+ ::SUBSCRIPTIONS_LIST = "#{API_PATH}/subscriptions.list" # camera_uuid
55
+ ::SUBSCRIPTIONS_DELETE = "#{API_PATH}/subscriptions.delete"
56
+ ::SUBSCRIPTIONS_CREATE_PUBLIC = "#{API_PATH}/subscriptions.create_public"
57
+ ::USERS_GET_SESSION_TOKEN = "#{API_PATH}/users.get_session_token"
58
+ ::USERS_GET_CURRENT = "#{API_PATH}/users.get_current"
59
+
60
+ ::CLIP_GET_ALL = "#{API_PATH}/videos.get_owned"
61
+ ::CLIP_CREATE = "#{API_PATH}/videos.request" # POST: start_date (ex. 1357598395), title, length (in seconds), uuid, description
62
+ ::CLIP_DELETE = "#{API_PATH}/videos.delete" # DELETE: id = clip_id
63
+
64
+ ## videos.get # id = clip_id
65
+ ## videos.download # id = clip_id
66
+ ## videos.play # id = clip_idß
67
+ def post(path, parameters, cookies, use_nexus=false)
68
+
69
+ http = _dropcam_http(use_nexus)
70
+
71
+ request = Net::HTTP::Post.new(path)
72
+ request.set_form_data(parameters)
73
+
74
+ request.add_field("Cookie",cookies.join('; ')) if cookies
75
+
76
+ response = http.request(request)
77
+
78
+ return response
79
+ end
80
+
81
+ def get(path, parameters,cookies, use_nexus=false)
82
+ http = _dropcam_http(use_nexus)
83
+
84
+ query_path = "#{path}"
85
+ query_path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters.length > 0
86
+ request = Net::HTTP::Get.new(query_path)
87
+
88
+ request.add_field("Cookie",cookies.join('; ')) if cookies
89
+
90
+ response = http.request(request)
91
+ return response
92
+ end
93
+
94
+ def delete(path, parameters,cookies, use_nexus=false)
95
+ http = _dropcam_http(use_nexus)
96
+
97
+ query_path = "#{path}"
98
+ query_path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters.length > 0
99
+ request = Net::HTTP::Delete.new(query_path)
100
+
101
+ request.add_field("Cookie",cookies.join('; ')) if cookies
102
+
103
+ response = http.request(request)
104
+ return response
105
+ end
106
+
107
+ protected
108
+
109
+ def _dropcam_http(use_nexus)
110
+ base = API_BASE
111
+ base = NEXUS_API_BASE if use_nexus
112
+ uri = URI.parse(base)
113
+ http = Net::HTTP.new(uri.host, uri.port)
114
+ http.use_ssl = true
115
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
116
+ #http.set_debug_output($stdout)
117
+
118
+ return http
119
+ end
120
+
121
+
122
+ end
123
+
124
+
125
+
126
+ end
@@ -0,0 +1,263 @@
1
+ require 'hpricot'
2
+
3
+ require_relative 'base'
4
+ require_relative 'error'
5
+ require_relative 'notification'
6
+ require_relative 'setting'
7
+ require_relative 'cuepoint'
8
+ require_relative 'clip'
9
+ require_relative 'stream'
10
+
11
+ module Dropcam
12
+ class Camera < Base
13
+
14
+ attr_reader :uuid, :notification_devices, :download_host, :download_server_live, :is_streaming, :title, :public_token
15
+ attr_reader :description, :timezone_utc_offset, :timezone, :is_connected, :is_online, :is_public, :hours_of_recording_max
16
+ attr_reader :type, :id, :owner_id
17
+ attr_accessor :settings
18
+
19
+
20
+ def initialize(uuid, properties={})
21
+ @uuid = uuid
22
+ @settings = {}
23
+ self.properties = properties
24
+ end
25
+
26
+ def properties=(properties)
27
+ properties.each{|key, value|
28
+ instance_variable_set("@#{key}", value)
29
+ }
30
+ end
31
+
32
+
33
+ def get_image(width=1200, timestamp=nil)
34
+ params = {"uuid"=>@uuid, "width" => width}
35
+ params["time"] = timestamp if timestamp
36
+
37
+ response = get(::IMAGE_PATH, params, @cookies, true)
38
+ if response.success?
39
+ return response.body
40
+ elsif response.not_authorized?
41
+ raise AuthorizationError
42
+ else
43
+ raise CameraNotFoundError
44
+ end
45
+ end
46
+
47
+ def current_image(width=1200)
48
+ return get_image(width)
49
+ end
50
+
51
+ def clips
52
+ response = get(::CLIP_GET_ALL, {}, @cookies)
53
+ if response.success?
54
+ return response.body ## returns a zip
55
+ clips = []
56
+ all_clips = JSON.parse(response.body)["items"]
57
+ for clip in all_clips
58
+ c = Clip.new(self, clip)
59
+ if c.camera_id == self.id
60
+ clips.push c
61
+ end
62
+ end
63
+ return clips
64
+ elsif response.not_authorized?
65
+ raise AuthorizationError
66
+ else
67
+ raise CameraNotFoundError
68
+ end
69
+ end
70
+
71
+
72
+ def create_clip(length, start_date, title, description)
73
+ params = {"uuid"=>@uuid, "length" => length, "start_date" => start_date, "title" => title, "description" => description}
74
+ response = post(::VIDEOS_REQUEST, params, @cookies)
75
+ if response.success?
76
+ clip_info = JSON.parse(response.body)["items"][0]
77
+ return Clip.new(self, clip_info)
78
+ elsif response.not_authorized?
79
+ raise AuthorizationError
80
+ else
81
+ raise CameraNotFoundError
82
+ end
83
+ end
84
+
85
+
86
+ def get_event_clip_image_archive(cuepoint_id, number_of_frames, width)
87
+ params = {"uuid"=>@uuid, "width" => width, "cuepoint_id" => cuepoint_id, "num_frames" => number_of_frames, "format" => "TAR_JPG"}
88
+ response = get(::NEXUS_GET_EVENT_CLIP_PATH, params, @cookies, true)
89
+ if response.success?
90
+ return response.body ## returns a zip
91
+ elsif response.not_authorized?
92
+ raise AuthorizationError
93
+ else
94
+ raise CameraNotFoundError
95
+ end
96
+ end
97
+
98
+ def get_event_clip_video(cuepoint_id, number_of_frames, width)
99
+ params = {"uuid"=>@uuid, "width" => width, "cuepoint_id" => cuepoint_id, "num_frames" => number_of_frames, "format" => "h264"}
100
+ response = get(::NEXUS_GET_EVENT_CLIP_PATH, params, @cookies, true)
101
+ if response.success?
102
+ return response.body ## returns a zip
103
+ elsif response.not_authorized?
104
+ raise AuthorizationError
105
+ else
106
+ raise CameraNotFoundError
107
+ end
108
+ end
109
+
110
+ def get_all_cuepoints(limit=2500)
111
+ params = {"uuid"=>@uuid, "max_results"=>limit}
112
+ response = get(::NEXUS_GET_REVERSE_PAGINATED_CUEPOINTS_PATH, params, @cookies, true)
113
+ if response.success?
114
+ cuepoints = []
115
+ all_cuepoints = JSON.parse(response.body)
116
+ for cuepoint in all_cuepoints
117
+ cuepoints.push Cuepoint.new(cuepoint)
118
+ end
119
+
120
+ return cuepoints
121
+ elsif response.not_authorized?
122
+ raise AuthorizationError
123
+ else
124
+ raise CameraNotFoundError
125
+ end
126
+ end
127
+
128
+ def get_cuepoint(start_time)
129
+ params = {"uuid"=>@uuid, "start_time" => start_time}
130
+ response = get(::NEXUS_GET_CUEPOINT_PATH, params, @cookies, true)
131
+ if response.success?
132
+ return Cuepoint.new(JSON.parse(response.body)[0])
133
+ elsif response.not_authorized?
134
+ raise AuthorizationError
135
+ else
136
+ raise CameraNotFoundError
137
+ end
138
+ end
139
+
140
+ def update_info
141
+ response = get(::CAMERAS_GET, {"id"=>@uuid}, @cookies)
142
+ if response.success?
143
+ self.properties = JSON.parse(response.body)["items"][0]
144
+ elsif response.not_authorized?
145
+ raise AuthorizationError
146
+ else
147
+ raise CameraNotFoundError
148
+ end
149
+ end
150
+
151
+
152
+
153
+ def public=(is_public)
154
+ response = post(::CAMERAS_UPDATE, {"uuid"=>@uuid, "is_public"=>is_public, "accepted_public_terms_at" => "true"}, @cookies)
155
+ if response.success?
156
+ self.properties = JSON.parse(response.body)["items"][0]
157
+ elsif response.not_authorized?
158
+ raise AuthorizationError
159
+ else
160
+ raise CameraNotFoundError
161
+ end
162
+ end
163
+
164
+ def public?
165
+ @is_public
166
+ end
167
+
168
+ def set_public_token(token)
169
+ response = post(::CAMERAS_UPDATE, {"uuid"=>@uuid, "token"=>token}, @cookies)
170
+ if response.success?
171
+ self.properties = JSON.parse(response.body)["items"][0]
172
+ return true
173
+ elsif response.not_authorized?
174
+ raise AuthorizationError
175
+ else
176
+ raise CameraNotFoundError
177
+ end
178
+ end
179
+
180
+ def settings=(new_settings)
181
+ @settings = {}
182
+ new_settings.each{|key,value|
183
+ @settings[key] = Setting.new(key, value, self)
184
+ }
185
+ @settings
186
+ end
187
+
188
+ def settings(force=false)
189
+ return @settings unless force == true or @settings.length == 0 # key these cached
190
+
191
+ response = get(::DROPCAMS_GET_PROPERTIES, {"uuid"=>@uuid}, @cookies)
192
+ if response.success?
193
+ self.settings = JSON.parse(response.body)["items"][0]
194
+ return @settings
195
+ elsif response.not_authorized?
196
+ raise AuthorizationError
197
+ else
198
+ raise CameraNotFoundError
199
+ end
200
+ end
201
+
202
+ def notification_devices
203
+
204
+ response = get(::CAMERA_FIND_NOTIFICATIONS, { "id" => @uuid }, @cookies)
205
+ if response.success?
206
+ notifications = []
207
+
208
+ all_notifications = JSON.parse(response.body)["items"]
209
+ all_notifications.each{|note|
210
+ notifications.push Notification.new(self, note["target"])
211
+ }
212
+
213
+ notifications
214
+ elsif response.not_authorized?
215
+ raise AuthorizationError
216
+ else
217
+ raise CameraNotFoundError
218
+ end
219
+ end
220
+
221
+ ## API for Notifications doesn't include email notifcations
222
+ ## The code below parses an HTML Partial to get all notifcation values
223
+
224
+ def all_notification_devices
225
+ request_path = ::CAMERA_HTML_SETTINGS_BASE + @uuid
226
+ response = get(request_path, {}, @cookies)
227
+ if response.success?
228
+ raw_html = response.body
229
+ doc = Hpricot.parse(raw_html)
230
+
231
+ notifications = []
232
+ doc.search("//div[@class='notification_target']").each { |notification_target|
233
+ puts notification_target
234
+ data_id = notification_target.get_attribute("data-id")
235
+ puts data_id
236
+ name = notification_target.at("div/span").inner_text
237
+
238
+ input = notification_target.at("div/input")
239
+
240
+ attributes = input.attributes.to_hash
241
+ data_type = attributes["data-type"]
242
+ data_value = attributes["data-value"]
243
+ checked = attributes.has_key?("checked")
244
+
245
+
246
+ notifications.push(note)
247
+ }
248
+ return notifications
249
+ elsif response.not_authorized?
250
+ raise AuthorizationError
251
+ else
252
+ raise CameraNotFoundError
253
+ end
254
+ end
255
+
256
+
257
+
258
+ def stream
259
+ Stream.new self
260
+ end
261
+
262
+ end
263
+ end
@@ -0,0 +1,52 @@
1
+ module Dropcam
2
+ class Clip
3
+ attr_reader :id, :public_link, :description, :title, :is_error, :start_time, :server, :camera_id, :generated_time
4
+ attr_reader :filename, :length_in_seconds
5
+
6
+ attr_accessor :properties
7
+ def initialize(camera, properties = nil)
8
+ @camera = camera
9
+ self.properties = properties
10
+ end
11
+
12
+ def properties=(properties)
13
+ properties.each{|key, value|
14
+ instance_variable_set("@#{key}", value)
15
+ }
16
+ @properties = properties
17
+ end
18
+
19
+ def direct_link
20
+ return "https://#{@server}/#{@filename}"
21
+ end
22
+ def direct_screenshot_link
23
+ return "https://#{@server}/s3#{File.basename(@filename, File.extname(@filename))}.jpg"
24
+ end
25
+
26
+ def set_title(title)
27
+ return false unless @id
28
+ response = post(::CLIP_DELETE, { "id" => @id, "title" => title }, @cookies)
29
+ if response.success?
30
+ return true
31
+ elsif response.not_authorized?
32
+ raise AuthorizationError
33
+ else
34
+ raise CameraNotFoundError
35
+ end
36
+
37
+ end
38
+
39
+ def delete
40
+ return false unless @id
41
+ response = post(::CLIP_DELETE, { "id" => @id }, @cookies)
42
+ if response.success?
43
+ return true
44
+ elsif response.not_authorized?
45
+ raise AuthorizationError
46
+ else
47
+ raise CameraNotFoundError
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,11 @@
1
+ module Dropcam
2
+ class Cuepoint
3
+ attr_reader :id, :note, :type, :time
4
+ def initialize(details)
5
+ @id = details["id"]
6
+ @note = details["note"]
7
+ @type = details["type"]
8
+ @time = details["time"]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module Dropcam
2
+ class AuthenticationError < StandardError
3
+ def initialize(msg = "Invalid Credentials")
4
+ super(msg)
5
+ end
6
+ end
7
+ class AuthorizationError < StandardError
8
+ def initialize(msg = "Not Authorized")
9
+ super(msg)
10
+ end
11
+ end
12
+ class CameraNotFoundError < StandardError
13
+ def initialize(msg = "Camera Not Found")
14
+ super(msg)
15
+ end
16
+ end
17
+ class UnkownError < StandardError
18
+ def initialize(msg = "Unkown Error")
19
+ super(msg)
20
+ end
21
+ end
22
+ class RequestError < StandardError
23
+ def initialize(msg = "Request Error")
24
+ super(msg)
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,66 @@
1
+ module Dropcam
2
+ class Notification < Base
3
+
4
+ attr_accessor :name, :type, :value, :is_enabled, :id
5
+
6
+ def initialize(camera, properties={})
7
+ @camera = camera
8
+ @name = properties["name"]
9
+ @type = properties["type"]
10
+ @value = properties["value"]
11
+ @id = properties["id"]
12
+ @is_enabled = properties["enabled"]
13
+ end
14
+
15
+ def find(name)
16
+ note = @camera.notification_devices.select{|note|
17
+ if note.name == name
18
+ puts "#{note.name} == #{name}"
19
+ return note
20
+ end
21
+ }
22
+ note
23
+ end
24
+
25
+ def create(email)
26
+ # {"status": 400, "items": [], "status_description": "bad-request", "status_detail": "This notification target already exists"}
27
+ response = post(::CAMERA_ADD_EMAIL_NOTIFICATION, {"email"=>email}, @camera.cookies)
28
+ if response.success?
29
+ return Notification.new(@camera, JSON.parse(response.body)["items"][0])
30
+ elsif response.error?
31
+ raise UnkownError, JSON.parse(response.body)["status_detail"]
32
+ elsif response.not_authorized?
33
+ raise AuthorizationError
34
+ else
35
+ raise CameraNotFoundError
36
+ end
37
+ end
38
+
39
+ def set(enable)
40
+ # email or gcm or apn
41
+ params = {"id"=>@camera.uuid, "is_enabled"=>enable, "device_token" => @value}
42
+ puts params
43
+ response = post(::CAMERA_NOTIFICATION_UPDATE, params, @camera.cookies)
44
+ if response.success?
45
+ return true
46
+ elsif response.not_authorized?
47
+ raise AuthorizationError
48
+ else
49
+ raise CameraNotFoundError
50
+ end
51
+ end
52
+
53
+ def delete(notifcation_id=nil)
54
+ notifcation_id = @id unless notifcation_id
55
+ response = post(::CAMERA_DELETE_NOTIFICATION, {"id"=>notifcation_id}, @camera.cookies)
56
+ if response.success?
57
+ return true
58
+ elsif response.not_authorized?
59
+ raise AuthorizationError
60
+ else
61
+ raise CameraNotFoundError
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'base'
2
+ module Dropcam
3
+ class Session < Base
4
+
5
+ attr_accessor :session_token, :cookies
6
+ def initialize(username, password)
7
+ @username = username
8
+ @password = password
9
+ end
10
+
11
+ def authenticate
12
+
13
+ params = {"username" => @username, "password" => @password}
14
+ response = post(::USERS_LOGIN, params, nil)
15
+ all_cookies = response.get_fields('set-cookie') # only cookies are set on valid credentials
16
+
17
+ ## for some reason, dropcam responds with 200 on invalid credentials
18
+ if response.success? and all_cookies
19
+
20
+ cookies = []
21
+ all_cookies.each { | cookie |
22
+ cookies.push(cookie.split('; ')[0])
23
+ }
24
+
25
+ @cookies = cookies
26
+ @session_token = _session_token # this value is embedded in the cookie but leaving this as is incase the API changes
27
+
28
+ else
29
+ raise AuthenticationError, "Invalid Credentials"
30
+ end
31
+ end
32
+
33
+
34
+
35
+ protected
36
+ def _session_token
37
+ response = get(::USERS_GET_SESSION_TOKEN, {}, @cookies)
38
+ if response.success?
39
+ response_json = JSON.parse(response.body)
40
+ token = response_json["items"][0]
41
+ return token
42
+ end
43
+ return nil
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ module Dropcam
2
+ class Setting < Base
3
+ #
4
+ attr_accessor :name
5
+ def initialize(name, value, camera)
6
+ @camera = camera
7
+ @name = name
8
+ @current_value = value
9
+ end
10
+
11
+ def value
12
+ return false if @current_value == 'false'
13
+ return true if @current_value == 'true'
14
+
15
+ @current_value
16
+ end
17
+
18
+ def to_s
19
+ "<Dropcam::Setting:#{object_id} @name=#{@name} @value=#{@current_value}>"
20
+ end
21
+
22
+ def set(value)
23
+ response = post(::DROPCAMS_SET_PROPERTY, {"uuid"=>@camera.uuid, "key" => @name, "value" => value}, @camera.cookies)
24
+ if response.success?
25
+ @current_value = value
26
+ @camera.settings = JSON.parse(response.body)["items"][0]
27
+ true
28
+ elsif response.error?
29
+ raise UnkownError, JSON.parse(response.body)["status_detail"]
30
+ elsif response.not_authorized?
31
+ raise AuthorizationError
32
+ else
33
+ raise CameraNotFoundError
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,96 @@
1
+ require 'Open3'
2
+ require 'timeout'
3
+ module Dropcam
4
+ class Stream
5
+ DEFAULT_RTSP_PORT = "554"
6
+ BUFFER_SIZE = 1024
7
+
8
+ attr_reader :camera
9
+ def initialize(camera)
10
+ @camera = camera
11
+ end
12
+ def rtsp_details
13
+ {
14
+ :protocol => "rtsp",
15
+ :user => "user",
16
+ :password => @camera.session_token,
17
+ :base => URI.parse(@camera.download_host).scheme,
18
+ :path => @camera.uuid,
19
+ :port => DEFAULT_RTSP_PORT
20
+ }
21
+ end
22
+ def rtsp_uri
23
+ URI.parse "rtsp://user:#{@camera.session_token}@#{URI.parse(@camera.download_host).scheme}:#{DEFAULT_RTSP_PORT}/#{@camera.uuid}"
24
+ end
25
+
26
+ def rtmp_details
27
+ {
28
+ :app => "nexus",
29
+ :host => "stream.dropcam.com",
30
+ :playpath => @camera.uuid,
31
+ :variables => { "S:" => @camera.session_token }
32
+ }
33
+ end
34
+
35
+ def rtmpdump
36
+ stream_command = "rtmpdump --live --app nexus --host stream.dropcam.com --playpath " + @camera.uuid
37
+ stream_command += " --conn S:" + @camera.session_token
38
+ return stream_command
39
+ end
40
+
41
+ def save_live(filename, duration=30)
42
+ raise StandardError, "RTMPDump is not found in your PATH" unless system("which -s rtmpdump")
43
+ # we add 10 seconds because it take about that amount of time to get the stream up and running with rtmpdump
44
+ run_with_timeout("#{self.rtmpdump} --quiet --flv #{filename}", duration+10, 1)
45
+ end
46
+
47
+ private
48
+
49
+ # Runs a specified shell command in a separate thread.
50
+ # If it exceeds the given timeout in seconds, kills it.
51
+ # Returns any output produced by the command (stdout or stderr) as a String.
52
+ # Uses Kernel.select to wait up to the tick length (in seconds) between
53
+ # checks on the command's status
54
+ #
55
+ # If you've got a cleaner way of doing this, I'd be interested to see it.
56
+ # If you think you can do it with Ruby's Timeout module, think again.
57
+
58
+ ## VIA https://gist.github.com/1032297
59
+ def run_with_timeout(command, timeout, tick)
60
+ output = ''
61
+ begin
62
+ # Start task in another thread, which spawns a process
63
+ stdin, stderrout, thread = Open3.popen2e(command)
64
+ # Get the pid of the spawned process
65
+ pid = thread[:pid]
66
+ start = Time.now
67
+
68
+ while (Time.now - start) < timeout and thread.alive?
69
+ # Wait up to `tick` seconds for output/error data
70
+ Kernel.select([stderrout], nil, nil, tick)
71
+ # Try to read the data
72
+ begin
73
+ #output << stderrout.read_nonblock(BUFFER_SIZE)
74
+ rescue IO::WaitReadable
75
+ # A read would block, so loop around for another select
76
+ rescue EOFError
77
+ # Command has completed, not really an error...
78
+ break
79
+ end
80
+ end
81
+ # Give Ruby time to clean up the other thread
82
+ sleep 1
83
+
84
+ if thread.alive?
85
+ # We need to kill the process, because killing the thread leaves
86
+ # the process alive but detached, annoyingly enough.
87
+ Process.kill("TERM", pid)
88
+ end
89
+ ensure
90
+ stdin.close if stdin
91
+ stderrout.close if stderrout
92
+ end
93
+ return output
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,3 @@
1
+ module Dropcam
2
+ VERSION = "0.0.1"
3
+ end
data/lib/dropcam.rb ADDED
@@ -0,0 +1,65 @@
1
+ require "dropcam/version"
2
+ require "dropcam/base"
3
+ require "dropcam/camera"
4
+ require "dropcam/session"
5
+
6
+ module Dropcam
7
+ class Dropcam < Base
8
+ attr_reader :session
9
+ def initialize(username, password)
10
+ @session = Session.new(username, password)
11
+ @session.authenticate
12
+ ##
13
+ end
14
+ def camera(uuid)
15
+ c = Camera.new(uuid)
16
+ c.cookies = @session.cookies
17
+ c.session_token = @session.session_token
18
+ c.properties = c.info
19
+ c
20
+ end
21
+
22
+ def get_public_camera(token)
23
+ response = get(::CAMERAS_GET_BY_PUBLIC_TOKEN, {"token"=>token, "return_deleted"=>true}, @session.cookies)
24
+ if response.success?
25
+ return response.body
26
+ elsif response.not_authorized?
27
+ raise AuthorizationError
28
+ else
29
+ raise CameraNotFoundError
30
+ end
31
+ end
32
+
33
+ def public_cameras
34
+ response = get(::CAMERAS_GET_PUBLIC, {}, @session.cookies)
35
+ cameras = []
36
+ if response.success?
37
+ response_json = JSON.parse(response.body)
38
+ owned = response_json["items"][0]["owned"]
39
+ owned.each{|camera|
40
+ c = Camera.new(camera["uuid"], camera)
41
+ c.cookies = @session.cookies
42
+ c.session_token = @session.session_token
43
+ cameras.push(c)
44
+ }
45
+ end
46
+ return cameras
47
+ end
48
+
49
+ def cameras
50
+ response = get(::CAMERAS_GET_VISIBLE, {"group_cameras" => true}, @session.cookies)
51
+ cameras = []
52
+ if response.success?
53
+ response_json = JSON.parse(response.body)
54
+ owned = response_json["items"][0]["owned"]
55
+ owned.each{|camera|
56
+ c = Camera.new(camera["uuid"], camera)
57
+ c.cookies = @session.cookies
58
+ c.session_token = @session.session_token
59
+ cameras.push(c)
60
+ }
61
+ end
62
+ return cameras
63
+ end
64
+ end
65
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dropcam
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nolan Brown
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-12 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Access Dropcam account and cameras
15
+ email:
16
+ - nolanbrown@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - dropcam.gemspec
27
+ - example/basics.rb
28
+ - example/camera.rb
29
+ - example/notifications.rb
30
+ - example/settings.rb
31
+ - example/stream.rb
32
+ - lib/dropcam.rb
33
+ - lib/dropcam/base.rb
34
+ - lib/dropcam/camera.rb
35
+ - lib/dropcam/clip.rb
36
+ - lib/dropcam/cuepoint.rb
37
+ - lib/dropcam/error.rb
38
+ - lib/dropcam/notification.rb
39
+ - lib/dropcam/session.rb
40
+ - lib/dropcam/setting.rb
41
+ - lib/dropcam/stream.rb
42
+ - lib/dropcam/version.rb
43
+ homepage: https://github.com/nolanbrown/dropcam
44
+ licenses: []
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubyforge_project:
63
+ rubygems_version: 1.8.24
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Access Dropcam account and cameras
67
+ test_files: []