summaryrefslogtreecommitdiff
path: root/plugin.video.ring_doorbell
diff options
context:
space:
mode:
authorJed Lippold <jlippold@users.noreply.github.com>2017-07-31 08:16:09 -0400
committerenen92 <enen92@users.noreply.github.com>2017-07-31 13:16:09 +0100
commite5508900b62962b76cd03f61e5dd5ca6ba2f492d (patch)
treece379ae3a0b541262fccce7763c332f11080fa55 /plugin.video.ring_doorbell
parent5a2098247be2066f13d5b17a511e44655458cbfb (diff)
[plugin.video.ring_doorbell] 1.0.1 (#1281)
* [plugin.video.ring_doorbell] 1.0.1 * changes for kodi repo inclusion * updates fanart, cache directory
Diffstat (limited to 'plugin.video.ring_doorbell')
-rwxr-xr-xplugin.video.ring_doorbell/LICENSE.txt340
-rw-r--r--plugin.video.ring_doorbell/README.md14
-rw-r--r--plugin.video.ring_doorbell/addon.xml22
-rw-r--r--plugin.video.ring_doorbell/changelog.txt11
-rw-r--r--plugin.video.ring_doorbell/default.py110
-rw-r--r--plugin.video.ring_doorbell/fanart.pngbin0 -> 54110 bytes
-rw-r--r--plugin.video.ring_doorbell/icon.pngbin0 -> 28623 bytes
-rw-r--r--plugin.video.ring_doorbell/resources/language/resource.language.en_gb/strings.po70
-rw-r--r--plugin.video.ring_doorbell/resources/settings.xml8
-rw-r--r--plugin.video.ring_doorbell/ring_doorbell/__init__.py677
-rw-r--r--plugin.video.ring_doorbell/ring_doorbell/const.py85
-rw-r--r--plugin.video.ring_doorbell/ring_doorbell/utils.py64
12 files changed, 1401 insertions, 0 deletions
diff --git a/plugin.video.ring_doorbell/LICENSE.txt b/plugin.video.ring_doorbell/LICENSE.txt
new file mode 100755
index 0000000..8cdb845
--- /dev/null
+++ b/plugin.video.ring_doorbell/LICENSE.txt
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {description}
+ Copyright (C) {year} {fullname}
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+
diff --git a/plugin.video.ring_doorbell/README.md b/plugin.video.ring_doorbell/README.md
new file mode 100644
index 0000000..cc3ca5e
--- /dev/null
+++ b/plugin.video.ring_doorbell/README.md
@@ -0,0 +1,14 @@
+Ring Video Doorbell
+=========
+
+Add on for Kodi to view doorbell recordings saved on ring.com.
+
+**Requires a ring.com subscription**
+
+- Supports playback of recorded events from ring.com for doorbell, floodlight and stickup cams.
+
+Credits
+===========
+
+Kodi add-on created by [jlippold](https://github.com/jlippold/) using python libraries created by [tchellomello](https://github.com/tchellomello/)
+
diff --git a/plugin.video.ring_doorbell/addon.xml b/plugin.video.ring_doorbell/addon.xml
new file mode 100644
index 0000000..26dbd2d
--- /dev/null
+++ b/plugin.video.ring_doorbell/addon.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<addon id="plugin.video.ring_doorbell" name="Ring video doorbell" version="1.0.2" provider-name="Jed Lippold, tchellomello">
+ <requires>
+ <import addon="xbmc.python" version="2.25.0" />
+ <import addon="script.module.requests" version="2.12.4" />
+ <import addon="script.module.dateutil" version="2.5.3" />
+ <import addon="script.module.pytz" version="2014.2" />
+ </requires>
+ <extension point="xbmc.python.pluginsource" library="default.py">
+ <provides>video</provides>
+ </extension>
+ <extension point="xbmc.addon.metadata">
+ <license>GNU General Public License, v2</license>
+ <description lang="en_GB">Play ring doorbell saved recording from Kodi. Requires a paid Ring.com subscription</description>
+ <platform>all</platform>
+ <website>https://github.com/jlippold</website>
+ <assets>
+ <icon>icon.png</icon>
+ <fanart>fanart.png</fanart>
+ </assets>
+ </extension>
+</addon> \ No newline at end of file
diff --git a/plugin.video.ring_doorbell/changelog.txt b/plugin.video.ring_doorbell/changelog.txt
new file mode 100644
index 0000000..4da807d
--- /dev/null
+++ b/plugin.video.ring_doorbell/changelog.txt
@@ -0,0 +1,11 @@
+1.0.2
+
+Updates fanart dimensions, and cache directory
+
+1.0.1
+
+Changes for main kodi repo
+
+1.0.0
+
+First commit. Supports doorbell and stick up cam recordings. \ No newline at end of file
diff --git a/plugin.video.ring_doorbell/default.py b/plugin.video.ring_doorbell/default.py
new file mode 100644
index 0000000..dce6382
--- /dev/null
+++ b/plugin.video.ring_doorbell/default.py
@@ -0,0 +1,110 @@
+import sys
+import urllib
+import urllib2
+import urlparse
+import xbmcgui
+import xbmcplugin
+import xbmcaddon
+import json
+import time
+import uuid
+import re
+
+from ring_doorbell import Ring
+from datetime import datetime
+from dateutil import tz
+
+
+base_url = sys.argv[0]
+addon_handle = int(sys.argv[1])
+args = urlparse.parse_qs(sys.argv[2][1:])
+ADDON = xbmcaddon.Addon(id='plugin.video.ring_doorbell')
+
+xbmcplugin.setContent(addon_handle, 'movies')
+mode = args.get('mode', None)
+
+def init():
+
+
+ email = ADDON.getSetting('email')
+ password = ADDON.getSetting('password')
+ items = ADDON.getSetting('items')
+
+ if len(email) <= 7:
+ return showModal(ADDON.getLocalizedString(30200))
+ if not re.match(r'[\w.-]+@[\w.-]+.\w+', email):
+ return showModal(ADDON.getLocalizedString(30200))
+ if len(password) <= 3:
+ return showModal(ADDON.getLocalizedString(30201))
+ if items.isdigit() == False:
+ return showModal(ADDON.getLocalizedString(30202))
+
+ try:
+ myring = Ring(email, password)
+ except:
+ return showModal(ADDON.getLocalizedString(30203))
+
+ if mode is None:
+ events = []
+ for device in list(myring.stickup_cams + myring.doorbells):
+ for event in device.history(limit=items):
+ event['formatted'] = format_event(device, event)
+ event['doorbell_id'] = device.id
+ events.append(event)
+ sorted_events = sorted(events, key=lambda k: k['id'], reverse=True)
+ for event in sorted_events:
+ url = build_url({'mode': 'play', 'doorbell_id': event['doorbell_id'], 'video_id': event['id']})
+ li = xbmcgui.ListItem(str(event['formatted']), iconImage='DefaultVideo.png')
+ xbmcplugin.addDirectoryItem(handle=addon_handle, url=url, listitem=li, isFolder=True)
+ xbmcplugin.endOfDirectory(addon_handle)
+ else:
+ if mode[0] == 'play':
+ doorbell_id = args['doorbell_id'][0]
+ video_id = args['video_id'][0]
+ for doorbell in list(myring.stickup_cams + myring.doorbells):
+ if doorbell.id == doorbell_id:
+ try:
+ url = doorbell.recording_url(video_id)
+ play_video(url)
+ except:
+ return showModal(ADDON.getLocalizedString(30204))
+
+def play_video(path):
+ if xbmc.Player().isPlaying():
+ xbmc.Player().stop()
+
+ play_item = xbmcgui.ListItem(path=path)
+ xbmc.Player().play(item=path, listitem=play_item)
+
+def build_url(query):
+ return base_url + '?' + urllib.urlencode(query)
+
+def build_url2(query):
+ return base_url + ', ' + urllib.urlencode(query)
+
+def format_event(device, event):
+ from_zone = tz.tzutc()
+ to_zone = tz.tzlocal()
+ utc = event['created_at'].replace(tzinfo=from_zone)
+ local_time = utc.astimezone(to_zone)
+
+ local_time_string = local_time.strftime('%I:%M %p ')
+ local_time_string += ADDON.getLocalizedString(30300)
+ local_time_string += local_time.strftime(' %A %b %d %Y')
+
+ event_name = ''
+ if event['kind'] == 'on_demand':
+ event_name = ADDON.getLocalizedString(30301)
+ if event['kind'] == 'motion':
+ event_name = ADDON.getLocalizedString(30302)
+ if event['kind'] == 'ding':
+ event_name = ADDON.getLocalizedString(30303)
+
+ return ' '.join([device.name, event_name, local_time_string])
+
+def showModal(text):
+ __addon__ = xbmcaddon.Addon()
+ __addonname__ = __addon__.getAddonInfo('name')
+ xbmcgui.Dialog().ok(__addonname__, text)
+
+init() \ No newline at end of file
diff --git a/plugin.video.ring_doorbell/fanart.png b/plugin.video.ring_doorbell/fanart.png
new file mode 100644
index 0000000..a5697c3
--- /dev/null
+++ b/plugin.video.ring_doorbell/fanart.png
Binary files differ
diff --git a/plugin.video.ring_doorbell/icon.png b/plugin.video.ring_doorbell/icon.png
new file mode 100644
index 0000000..2740f43
--- /dev/null
+++ b/plugin.video.ring_doorbell/icon.png
Binary files differ
diff --git a/plugin.video.ring_doorbell/resources/language/resource.language.en_gb/strings.po b/plugin.video.ring_doorbell/resources/language/resource.language.en_gb/strings.po
new file mode 100644
index 0000000..6551b2b
--- /dev/null
+++ b/plugin.video.ring_doorbell/resources/language/resource.language.en_gb/strings.po
@@ -0,0 +1,70 @@
+# Kodi Media Center language file
+# Addon Name: Ring Video Doorbell
+# Addon id: plugin.video.ring_doorbell
+# Addon Provider: jlippold
+msgid ""
+msgstr ""
+"Project-Id-Version: Kodi Addons\n"
+"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n"
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Kodi Translation Team\n"
+"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgctxt "#30000"
+msgid "Account Info"
+msgstr ""
+
+msgctxt "#30001"
+msgid "Email"
+msgstr ""
+
+msgctxt "#30002"
+msgid "Password"
+msgstr ""
+
+msgctxt "#30003"
+msgid "Max Videos"
+msgstr ""
+
+
+msgctxt "#30200"
+msgid "Invalid email address, check addon settings"
+msgstr ""
+
+msgctxt "#30201"
+msgid "Invalid password, check addon settings"
+msgstr ""
+
+msgctxt "#30202"
+msgid "Invalid item count, check addon settings"
+msgstr ""
+
+msgctxt "#30203"
+msgid "Authentication Error: Check your ring.com credentials"
+msgstr ""
+
+msgctxt "#30204"
+msgid "Error playing video, it is possible that the video was not found on Ring.com"
+msgstr ""
+
+msgctxt "#30300"
+msgid "on"
+msgstr ""
+
+msgctxt "#30301"
+msgid "Live View at"
+msgstr ""
+
+msgctxt "#30302"
+msgid "Motion at"
+msgstr ""
+
+msgctxt "#30303"
+msgid "Ring at"
+msgstr ""
diff --git a/plugin.video.ring_doorbell/resources/settings.xml b/plugin.video.ring_doorbell/resources/settings.xml
new file mode 100644
index 0000000..bcf5748
--- /dev/null
+++ b/plugin.video.ring_doorbell/resources/settings.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<settings>
+ <category label="30000">
+ <setting label="30001" type="text" id="email" default=""/>
+ <setting label="30002" type="text" id="password" option="hidden" default=""/>
+ <setting label="30003" type="slider" id="items" subsetting="true" default="50" range="5,5,100" option="int" />
+ </category>
+</settings> \ No newline at end of file
diff --git a/plugin.video.ring_doorbell/ring_doorbell/__init__.py b/plugin.video.ring_doorbell/ring_doorbell/__init__.py
new file mode 100644
index 0000000..85e8eca
--- /dev/null
+++ b/plugin.video.ring_doorbell/ring_doorbell/__init__.py
@@ -0,0 +1,677 @@
+# coding: utf-8
+# vim:sw=4:ts=4:et:
+"""Python Ring Doorbell wrapper."""
+from datetime import datetime
+
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
+
+import os
+import logging
+import requests
+import pytz
+
+from ring_doorbell.utils import (
+ _locator, _exists_cache, _save_cache, _read_cache)
+from ring_doorbell.const import (
+ API_VERSION, API_URI, CACHE_ATTRS, CACHE_FILE, CHIMES_ENDPOINT,
+ CHIME_VOL_MIN, CHIME_VOL_MAX,
+ DEVICES_ENDPOINT, DOORBELLS_ENDPOINT, DOORBELL_VOL_MIN, DOORBELL_VOL_MAX,
+ DOORBELL_EXISTING_TYPE, DINGS_ENDPOINT, FILE_EXISTS,
+ HEADERS, LINKED_CHIMES_ENDPOINT, LIVE_STREAMING_ENDPOINT,
+ NEW_SESSION_ENDPOINT, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE,
+ MSG_GENERIC_FAIL, MSG_VOL_OUTBOUND,
+ NOT_FOUND, URL_DOORBELL_HISTORY, URL_RECORDING,
+ POST_DATA, PERSIST_TOKEN_ENDPOINT, PERSIST_TOKEN_DATA,
+ RETRY_TOKEN, TESTSOUND_CHIME_ENDPOINT)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Ring(object):
+ """A Python Abstraction object to Ring Door Bell."""
+
+ def __init__(self, username, password, debug=False, persist_token=False,
+ push_token_notify_url="http://localhost/", reuse_session=True,
+ cache_file=CACHE_FILE):
+ """Initialize the Ring object."""
+ self.is_connected = None
+ self.token = None
+ self.params = None
+ self._persist_token = persist_token
+ self._push_token_notify_url = push_token_notify_url
+
+ self.debug = debug
+ self.username = username
+ self.password = password
+ self.session = requests.Session()
+ self.session.auth = (self.username, self.password)
+
+ self.cache = CACHE_ATTRS
+ self.cache['account'] = self.username
+ self.cache_file = cache_file
+ self._reuse_session = reuse_session
+
+ # tries to re-use old session
+ if self._reuse_session:
+ self.cache['token'] = self.token
+ self._process_cached_session()
+ else:
+ self._authenticate()
+
+ def _process_cached_session(self):
+ """Process cache_file to reuse token instead."""
+ if _exists_cache(self.cache_file):
+ self.cache = _read_cache(self.cache_file)
+
+ # if self.cache['token'] is None, the cache file was corrupted.
+ # of if self.cache['account'] does not match with self.username
+ # In both cases, a new auth token is required.
+ if (self.cache['token'] is None) or \
+ (self.cache['account'] is None) or \
+ (self.cache['account'] != self.username):
+ self._authenticate()
+ else:
+ # we need to set the self.token and self.params
+ # to make use of the self.query() method
+ self.token = self.cache['token']
+ self.params = {'api_version': API_VERSION,
+ 'auth_token': self.token}
+
+ # test if token from cache_file is still valid and functional
+ # if not, it should continue to get a new auth token
+ url = API_URI + DEVICES_ENDPOINT
+ req = self.query(url, raw=True)
+ if req.status_code == 200:
+ self._authenticate(session=req)
+ else:
+ self._authenticate()
+ else:
+ # first time executing, so we have to create a cache file
+ self._authenticate()
+
+ def _authenticate(self, attempts=RETRY_TOKEN, session=None):
+ """Authenticate user against Ring API."""
+ url = API_URI + NEW_SESSION_ENDPOINT
+
+ loop = 0
+ while loop <= attempts:
+ loop += 1
+ try:
+ if session is None:
+ req = self.session.post((url),
+ data=POST_DATA,
+ headers=HEADERS)
+ else:
+ req = session
+ except:
+ raise
+
+ # if token is expired, refresh credentials and try again
+ if req.status_code == 200 or req.status_code == 201:
+
+ # the only way to get a JSON with token is via POST,
+ # so we need a special conditional for 201 code
+ if req.status_code == 201:
+ data = req.json().get('profile')
+ self.token = data.get('authentication_token')
+
+ self.is_connected = True
+ self.params = {'api_version': API_VERSION,
+ 'auth_token': self.token}
+
+ if self._persist_token and self._push_token_notify_url:
+ url = API_URI + PERSIST_TOKEN_ENDPOINT
+ PERSIST_TOKEN_DATA['auth_token'] = self.token
+ PERSIST_TOKEN_DATA['device[push_notification_token]'] = \
+ self._push_token_notify_url
+ req = self.session.put((url), headers=HEADERS,
+ data=PERSIST_TOKEN_DATA)
+
+ # update token if reuse_session is True
+ if self._reuse_session:
+ self.cache['account'] = self.username
+ self.cache['token'] = self.token
+
+ _save_cache(self.cache, self.cache_file)
+ return True
+
+ self.is_connected = False
+ req.raise_for_status()
+
+ def query(self,
+ url,
+ attempts=RETRY_TOKEN,
+ method='GET',
+ raw=False,
+ extra_params=None):
+ """Query data from Ring API."""
+ if self.debug:
+ _LOGGER.debug("Querying %s", url)
+
+ if self.debug and not self.is_connected:
+ _LOGGER.debug("Not connected. Refreshing token...")
+ self._authenticate()
+
+ response = None
+ loop = 0
+ while loop <= attempts:
+ if self.debug:
+ _LOGGER.debug("running query loop %s", loop)
+
+ # allow to override params when necessary
+ # and update self.params globally for the next connection
+ if extra_params:
+ params = self.params
+ params.update(extra_params)
+ else:
+ params = self.params
+
+ loop += 1
+ try:
+ if method == 'GET':
+ req = self.session.get((url), params=urlencode(params))
+ elif method == 'PUT':
+ req = self.session.put((url), params=urlencode(params))
+ elif method == 'POST':
+ req = self.session.post((url), params=urlencode(params))
+
+ if self.debug:
+ _LOGGER.debug("_query %s ret %s", loop, req.status_code)
+ except:
+ raise
+
+ # if token is expired, refresh credentials and try again
+ if req.status_code == 401:
+ self.is_connected = False
+ self._authenticate()
+ continue
+
+ if req.status_code == 200 or req.status_code == 204:
+ # if raw, return session object otherwise return JSON
+ if raw:
+ response = req
+ else:
+ if method == 'GET':
+ response = req.json()
+ break
+
+ if self.debug:
+ _LOGGER.debug("%s", MSG_GENERIC_FAIL)
+ return response
+
+ @property
+ def devices(self):
+ """Return all devices."""
+ devs = {}
+ devs['chimes'] = self.chimes
+ devs['stickup_cams'] = self.stickup_cams
+ devs['doorbells'] = self.doorbells
+ return devs
+
+ def __devices(self, device_type):
+ """Private method to query devices."""
+ lst = []
+ url = API_URI + DEVICES_ENDPOINT
+ try:
+ if device_type == 'stickup_cams':
+ req = self.query(url).get('stickup_cams')
+ for member in list((obj['description'] for obj in req)):
+ lst.append(RingStickUpCam(self, member))
+
+ if device_type == 'chime':
+ req = self.query(url).get('chimes')
+ for member in list((obj['description'] for obj in req)):
+ lst.append(RingChime(self, member))
+
+ if device_type == 'doorbell':
+ req = self.query(url).get('doorbots')
+ for member in list((obj['description'] for obj in req)):
+ lst.append(RingDoorBell(self, member))
+
+ # get shared doorbells, however device is read-only
+ req = self.query(url).get('authorized_doorbots')
+ for member in list((obj['description'] for obj in req)):
+ lst.append(RingDoorBell(self, member, shared=True))
+
+ except AttributeError:
+ pass
+ return lst
+
+ @property
+ def chimes(self):
+ """Return a list of RingDoorChime objects."""
+ return self.__devices('chime')
+
+ @property
+ def stickup_cams(self):
+ """Return a list of RingStickUpCam objects."""
+ return self.__devices('stickup_cams')
+
+ @property
+ def doorbells(self):
+ """Return a list of RingDoorBell objects."""
+ return self.__devices('doorbell')
+
+
+class RingGeneric(object):
+ """Generic Implementation for Ring Chime/Doorbell."""
+
+ def __init__(self):
+ """Initialize Ring Generic."""
+ self._attrs = None
+ self._ring = None
+ self.debug = None
+ self.family = None
+ self.name = None
+
+ # alerts notifications
+ self.alert_expires_at = None
+
+ def __repr__(self):
+ """Return __repr__."""
+ return "<{0}: {1}>".format(self.__class__.__name__, self.name)
+
+ def update(self):
+ """Refresh attributes."""
+ self._get_attrs()
+ self._update_alert()
+
+ @property
+ def alert(self):
+ """Return alert attribute."""
+ return self._ring.cache['alerts']
+
+ @alert.setter
+ def alert(self, value):
+ """Set attribute to alert."""
+ self._ring.cache['alerts'] = value
+ _save_cache(self._ring.cache, self._ring.cache_file)
+ return True
+
+ def _update_alert(self):
+ """Verify if alert received is still valid."""
+ # alert is no longer valid
+ if self.alert and self.alert_expires_at:
+ if datetime.now() >= self.alert_expires_at:
+ self.alert = None
+ self.alert_expires_at = None
+ _save_cache(self._ring.cache, self._ring.cache_file)
+
+ def _get_attrs(self):
+ """Return attributes."""
+ url = API_URI + DEVICES_ENDPOINT
+ try:
+ if self.family == 'doorbots' and self.shared:
+ lst = self._ring.query(url).get('authorized_doorbots')
+ else:
+ lst = self._ring.query(url).get(self.family)
+ index = _locator(lst, 'description', self.name)
+ if index == NOT_FOUND:
+ return None
+ except AttributeError:
+ return None
+
+ self._attrs = lst[index]
+ return True
+
+ @property
+ def account_id(self):
+ """Return account ID."""
+ return self._attrs.get('id')
+
+ @property
+ def address(self):
+ """Return address."""
+ return self._attrs.get('address')
+
+ @property
+ def firmware(self):
+ """Return firmware."""
+ return self._attrs.get('firmware_version')
+
+ # pylint: disable=invalid-name
+ @property
+ def id(self):
+ """Return ID."""
+ return self._attrs.get('device_id')
+
+ @property
+ def latitude(self):
+ """Return latitude attr."""
+ return self._attrs.get('latitude')
+
+ @property
+ def longitude(self):
+ """Return longitude attr."""
+ return self._attrs.get('longitude')
+
+ @property
+ def kind(self):
+ """Return kind attr."""
+ return self._attrs.get('kind')
+
+ @property
+ def timezone(self):
+ """Return timezone."""
+ return self._attrs.get('time_zone')
+
+
+class RingChime(RingGeneric):
+ """Implementation for Ring Chime."""
+
+ def __init__(self, ring, name):
+ """Initilize Ring chime object."""
+ super(RingChime, self).__init__()
+ self._attrs = None
+ self._ring = ring
+ self.debug = self._ring.debug
+ self.family = 'chimes'
+ self.name = name
+ self.update()
+
+ @property
+ def volume(self):
+ """Return if chime volume."""
+ return self._attrs.get('settings').get('volume')
+
+ @volume.setter
+ def volume(self, value):
+ if not ((isinstance(value, int)) and
+ (value >= CHIME_VOL_MIN and value <= CHIME_VOL_MAX)):
+ _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(CHIME_VOL_MIN,
+ CHIME_VOL_MAX))
+ return False
+
+ params = {
+ 'chime[description]': self.name,
+ 'chime[settings][volume]': str(value)}
+ url = API_URI + CHIMES_ENDPOINT.format(self.account_id)
+ self._ring.query(url, extra_params=params, method='PUT')
+ self.update()
+ return True
+
+ @property
+ def linked_tree(self):
+ """Return doorbell data linked to chime."""
+ url = API_URI + LINKED_CHIMES_ENDPOINT.format(self.account_id)
+ return self._ring.query(url)
+
+ @property
+ def test_sound(self):
+ """Play chime to test sound."""
+ url = API_URI + TESTSOUND_CHIME_ENDPOINT.format(self.account_id)
+ self._ring.query(url, method='POST')
+ return True
+
+class RingDoorBell(RingGeneric):
+ """Implementation for Ring Doorbell."""
+
+ def __init__(self, ring, name, shared=False):
+ """Initilize Ring doorbell object."""
+ super(RingDoorBell, self).__init__()
+ self._attrs = None
+ self._ring = ring
+ self.shared = shared
+ self.debug = self._ring.debug
+ self.family = 'doorbots'
+ self.name = name
+ self.update()
+
+ @property
+ def battery_life(self):
+ """Return battery life."""
+ value = int(self._attrs.get('battery_life'))
+ if value > 100:
+ value = 100
+ return value
+
+ def check_alerts(self):
+ """Return JSON when motion or ring is detected."""
+ url = API_URI + DINGS_ENDPOINT
+ self.update()
+
+ try:
+ resp = self._ring.query(url)[0]
+ except (IndexError, TypeError):
+ return None
+
+ if resp:
+ timestamp = resp.get('now') + resp.get('expires_in')
+ self.alert = resp
+ self.alert_expires_at = datetime.fromtimestamp(timestamp)
+
+ # save to a pickle data
+ if self.alert:
+ _save_cache(self._ring.cache, self._ring.cache_file)
+ return True
+ return None
+
+ @property
+ def existing_doorbell_type(self):
+ """
+ Return existing doorbell type.
+
+ 0: Mechanical
+ 1: Digital
+ 2: Not Present
+ """
+ try:
+ return DOORBELL_EXISTING_TYPE[
+ self._attrs.get('settings').get('chime_settings').get('type')]
+ except AttributeError:
+ return None
+
+ @existing_doorbell_type.setter
+ def existing_doorbell_type(self, value):
+ """
+ Return existing doorbell type.
+
+ 0: Mechanical
+ 1: Digital
+ 2: Not Present
+ """
+ if value not in DOORBELL_EXISTING_TYPE.keys():
+ _LOGGER.error("%s", MSG_EXISTING_TYPE)
+ return False
+ params = {
+ 'doorbot[description]': self.name,
+ 'doorbot[settings][chime_settings][type]': value}
+ if self.existing_doorbell_type:
+ url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id)
+ self._ring.query(url, extra_params=params, method='PUT')
+ self.update()
+ return True
+ return None
+
+ @property
+ def existing_doorbell_type_enabled(self):
+ """Return if existing doorbell type is enabled."""
+ if self.existing_doorbell_type:
+ if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]:
+ return None
+ return \
+ self._attrs.get('settings').get('chime_settings').get('enable')
+ return False
+
+ @existing_doorbell_type_enabled.setter
+ def existing_doorbell_type_enabled(self, value):
+ """Enable/disable the existing doorbell if Digital/Mechanical."""
+ if self.existing_doorbell_type:
+
+ if not isinstance(value, bool):
+ _LOGGER.error("%s", MSG_BOOLEAN_REQUIRED)
+ return None
+
+ if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]:
+ return None
+
+ params = {
+ 'doorbot[description]': self.name,
+ 'doorbot[settings][chime_settings][enable]': value}
+ url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id)
+ self._ring.query(url, extra_params=params, method='PUT')
+ self.update()
+ return True
+ return False
+
+ @property
+ def existing_doorbell_type_duration(self):
+ """Return duration for Digital chime."""
+ if self.existing_doorbell_type:
+ if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1]:
+ return self._attrs.get('settings').\
+ get('chime_settings').get('duration')
+ return None
+
+ @existing_doorbell_type_duration.setter
+ def existing_doorbell_type_duration(self, value):
+ """Set duration for Digital chime."""
+ if self.existing_doorbell_type:
+
+ if not ((isinstance(value, int)) and
+ (value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)):
+ _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN,
+ DOORBELL_VOL_MAX))
+ return False
+
+ if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1]:
+ params = {
+ 'doorbot[description]': self.name,
+ 'doorbot[settings][chime_settings][duration]': value}
+ url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id)
+ self._ring.query(url, extra_params=params, method='PUT')
+ self.update()
+ return True
+ return None
+
+ def history(self, limit=30, timezone=None, kind=None):
+ """Return history with datetime objects."""
+ # allow modify the items to return
+ params = {'limit': str(limit)}
+
+ url = API_URI + URL_DOORBELL_HISTORY.format(self.account_id)
+ response = self._ring.query(url, extra_params=params)
+
+ # convert for specific timezone
+ utc = pytz.utc
+ if timezone:
+ mytz = pytz.timezone(timezone)
+
+ for entry in response:
+ dt_at = datetime.strptime(entry['created_at'],
+ '%Y-%m-%dT%H:%M:%S.000Z')
+ utc_dt = datetime(dt_at.year, dt_at.month, dt_at.day, dt_at.hour,
+ dt_at.minute, dt_at.second, tzinfo=utc)
+ if timezone:
+ tz_dt = utc_dt.astimezone(mytz)
+ entry['created_at'] = tz_dt
+ else:
+ entry['created_at'] = utc_dt
+
+ if kind:
+ return list(filter(lambda array: array['kind'] == kind, response))
+
+ return response
+
+ @property
+ def last_recording_id(self):
+ """Return the last recording ID."""
+ try:
+ return self.history(limit=1)[0]['id']
+ except (IndexError, TypeError):
+ return None
+
+ @property
+ def live_streaming_json(self):
+ """Return JSON for live streaming."""
+ url = API_URI + LIVE_STREAMING_ENDPOINT.format(self.account_id)
+ req = self._ring.query((url), method='POST', raw=True)
+ if req.status_code == 204:
+ url = API_URI + DINGS_ENDPOINT
+ try:
+ return self._ring.query(url)[0]
+ except (IndexError, TypeError):
+ pass
+ return None
+
+ def recording_download(self, recording_id, filename=None, override=False):
+ """Save a recording in MP4 format to a file or return raw."""
+ url = API_URI + URL_RECORDING.format(recording_id)
+ try:
+ req = self._ring.query(url, raw=True)
+ if req.status_code == 200:
+
+ if filename:
+ if os.path.isfile(filename) and not override:
+ _LOGGER.error("%s", FILE_EXISTS.format(filename))
+ return False
+
+ with open(filename, 'wb') as recording:
+ recording.write(req.content)
+ return True
+ else:
+ return req.content
+ except IOError as error:
+ _LOGGER.error("%s", error)
+ raise
+
+ def recording_url(self, recording_id):
+ """Return HTTPS recording URL."""
+ url = API_URI + URL_RECORDING.format(recording_id)
+ req = self._ring.query(url, raw=True)
+ if req.status_code == 200:
+ return req.url
+ return False
+
+ @property
+ def subscribed(self):
+ """Return if is online."""
+ result = self._attrs.get('subscribed')
+ if result is None:
+ return False
+ return True
+
+ @property
+ def subscribed_motion(self):
+ """Return if is subscribed_motion."""
+ result = self._attrs.get('subscribed_motions')
+ if result is None:
+ return False
+ return True
+
+ @property
+ def volume(self):
+ """Return volume."""
+ return self._attrs.get('settings').get('doorbell_volume')
+
+ @volume.setter
+ def volume(self, value):
+ if not ((isinstance(value, int)) and
+ (value >= DOORBELL_VOL_MIN and value <= DOORBELL_VOL_MAX)):
+ _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN,
+ DOORBELL_VOL_MAX))
+ return False
+
+ params = {
+ 'doorbot[description]': self.name,
+ 'doorbot[settings][doorbell_volume]': str(value)}
+ url = API_URI + DOORBELLS_ENDPOINT.format(self.account_id)
+ self._ring.query(url, extra_params=params, method='PUT')
+ self.update()
+ return True
+
+
+class RingStickUpCam(RingDoorBell):
+ """Implementation for Ring RingStickUpCam."""
+
+ def __init__(self, ring, name):
+ super(RingDoorBell, self).__init__()
+ self._attrs = None
+ self._ring = ring
+ self.debug = self._ring.debug
+ self.family = 'stickup_cams'
+ self.name = name
+ self.update()
diff --git a/plugin.video.ring_doorbell/ring_doorbell/const.py b/plugin.video.ring_doorbell/ring_doorbell/const.py
new file mode 100644
index 0000000..d0c9e99
--- /dev/null
+++ b/plugin.video.ring_doorbell/ring_doorbell/const.py
@@ -0,0 +1,85 @@
+# coding: utf-8
+# vim:sw=4:ts=4:et:
+"""Constants."""
+import os
+import xbmc, xbmcaddon
+from uuid import uuid4 as uuid
+
+HEADERS = {'Content-Type': 'application/x-www-form-urlencoded; charset: UTF-8',
+ 'User-Agent': 'Dalvik/1.6.0 (Linux; Android 4.4.4; Build/KTU84Q)',
+ 'Accept-Encoding': 'gzip, deflate'}
+
+# number of attempts to refresh token
+RETRY_TOKEN = 3
+
+# default suffix for session cache file
+CACHE_ATTRS = {'account': None, 'alerts': None, 'token': None}
+
+ADDON = xbmcaddon.Addon(id='plugin.video.ring_doorbell')
+CACHE_FILE = xbmc.translatePath(os.path.join(ADDON.getAddonInfo('profile').decode("utf-8"), '.ring_doorbell-session.cache'))
+
+# code when item was not found
+NOT_FOUND = -1
+
+# API endpoints
+API_VERSION = '9'
+API_URI = 'https://api.ring.com'
+CHIMES_ENDPOINT = '/clients_api/chimes/{0}'
+DEVICES_ENDPOINT = '/clients_api/ring_devices'
+DINGS_ENDPOINT = '/clients_api/dings/active'
+DOORBELLS_ENDPOINT = '/clients_api/doorbots/{0}'
+PERSIST_TOKEN_ENDPOINT = '/clients_api/device'
+
+LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + '/linked_doorbots'
+LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + '/vod'
+NEW_SESSION_ENDPOINT = '/clients_api/session'
+TESTSOUND_CHIME_ENDPOINT = CHIMES_ENDPOINT + '/play_sound'
+URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + '/history'
+URL_RECORDING = '/clients_api/dings/{0}/recording'
+
+# default values
+CHIME_VOL_MIN = 0
+CHIME_VOL_MAX = 10
+
+DOORBELL_VOL_MIN = 0
+DOORBELL_VOL_MAX = 11
+
+DOORBELL_EXISTING_TYPE = {
+ 0: 'Mechanical',
+ 1: 'Digital',
+ 2: 'Not Present'}
+
+# error strings
+MSG_BOOLEAN_REQUIRED = "Boolean value is required."
+MSG_EXISTING_TYPE = "Integer value where {0}.".format(DOORBELL_EXISTING_TYPE)
+MSG_GENERIC_FAIL = 'Sorry.. Something went wrong...'
+FILE_EXISTS = 'The file {0} already exists.'
+MSG_VOL_OUTBOUND = 'Must be within the {0}-{1}.'
+
+# structure acquired from reverse engineering to create auth token
+POST_DATA = {
+ 'api_version': API_VERSION,
+ 'device[hardware_id]': str(uuid()),
+ 'device[os]': 'android',
+ 'device[app_brand]': 'ring',
+ 'device[metadata][device_model]': 'KVM',
+ 'device[metadata][device_name]': 'Python',
+ 'device[metadata][resolution]': '600x800',
+ 'device[metadata][app_version]': '1.3.806',
+ 'device[metadata][app_instalation_date]': '',
+ 'device[metadata][manufacturer]': 'Qemu',
+ 'device[metadata][device_type]': 'desktop',
+ 'device[metadata][architecture]': 'desktop',
+ 'device[metadata][language]': 'en'}
+
+PERSIST_TOKEN_DATA = {
+ 'api_version': API_VERSION,
+ 'device[metadata][device_model]': 'KVM',
+ 'device[metadata][device_name]': 'Python',
+ 'device[metadata][resolution]': '600x800',
+ 'device[metadata][app_version]': '1.3.806',
+ 'device[metadata][app_instalation_date]': '',
+ 'device[metadata][manufacturer]': 'Qemu',
+ 'device[metadata][device_type]': 'desktop',
+ 'device[metadata][architecture]': 'x86',
+ 'device[metadata][language]': 'en'}
diff --git a/plugin.video.ring_doorbell/ring_doorbell/utils.py b/plugin.video.ring_doorbell/ring_doorbell/utils.py
new file mode 100644
index 0000000..0348a77
--- /dev/null
+++ b/plugin.video.ring_doorbell/ring_doorbell/utils.py
@@ -0,0 +1,64 @@
+# coding: utf-8
+# vim:sw=4:ts=4:et:
+"""Python Ring Doorbell utils."""
+import os
+from ring_doorbell.const import CACHE_ATTRS, NOT_FOUND
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+
+def _locator(lst, key, value):
+ """Return the position of a match item in list."""
+ try:
+ return next(index for (index, d) in enumerate(lst)
+ if d[key] == value)
+ except StopIteration:
+ return NOT_FOUND
+
+
+def _clean_cache(filename):
+ """Remove filename if pickle version mismatch."""
+ try:
+ if os.path.isfile(filename):
+ os.remove(filename)
+ except:
+ raise
+
+ # initialize cache since file was removed
+ initial_cache_data = CACHE_ATTRS
+ _save_cache(initial_cache_data, filename)
+ return initial_cache_data
+
+
+def _exists_cache(filename):
+ """Check if filename exists and if is pickle object."""
+ return bool(os.path.isfile(filename))
+
+
+def _save_cache(data, filename):
+ """Dump data into a pickle file."""
+ try:
+ with open(filename, 'wb') as pickle_db:
+ pickle.dump(data, pickle_db)
+ return True
+ except:
+ raise
+
+
+def _read_cache(filename):
+ """Read data from a pickle file."""
+ try:
+ if os.path.isfile(filename):
+ data = pickle.load(open(filename, 'rb'))
+
+ # make sure pickle obj has the expected defined keys
+ # if not reinitialize cache
+ if data.keys() != CACHE_ATTRS.keys():
+ raise EOFError
+ return data
+
+ except (EOFError, ValueError):
+ return _clean_cache(filename)