├── .gitignore ├── .startscript.sh ├── License.md ├── Motion Detector Camera with ePIR (discontinued part).fzz ├── Motion Detector Camera.fzz ├── README.md ├── Raspberry Pi Costing.xls ├── __make_executable ├── _showproc ├── doorbell2.ogg ├── install_python_linphone.sh ├── jampi.png ├── jampi.svg ├── security_camera.py └── security_off /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | .~lock.Raspberry Pi Costing.xls# 3 | *.xls# 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | #Ipython Notebook 66 | .ipynb_checkpoints 67 | -------------------------------------------------------------------------------- /.startscript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #curl wttr.in/SGF 3 | #sleep 5 4 | # You probably want to remove the line with 'sudo rm...', unless you really do 5 | # want to delete the log file every time. 6 | # The double 'clear' commands are due to a quirk in Putty--it always keeps 7 | # one screenfull in the scrollback buffer unless you do this. 8 | echo 'Stopping any currently running security_camera.py Python script...' 9 | ./security_off 10 | sleep 3 11 | # Comment-out the next two lines if you always want to append to the log. 12 | # You should monitor the disk space and move the log file across the network/to a USB. 13 | echo 'Cleaning log...' 14 | sudo rm /var/log/security_camera.log; clear; clear 15 | echo 'Starting security_camera.py, tailing the log.' 16 | sudo python security_camera.py >>security_camera_log 2>&1 & 17 | sleep 3 18 | tail -f /var/log/security_camera.log 19 | cp /var/log/security_camera.log ./ 20 | #~/ngrok tcp 22 21 | 22 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | **The MIT License (MIT)** 2 | 3 | Copyright (c) 2016 Leland Green 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Motion Detector Camera with ePIR (discontinued part).fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelandg/Raspberry-Pi-Security-Camera/978899c9d111c4d53e6fdb41efc6bca119398d80/Motion Detector Camera with ePIR (discontinued part).fzz -------------------------------------------------------------------------------- /Motion Detector Camera.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelandg/Raspberry-Pi-Security-Camera/978899c9d111c4d53e6fdb41efc6bca119398d80/Motion Detector Camera.fzz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry-Pi-Security-Camera 2 | ![JamPi Logo](jampi.png) 3 | 4 | Copyright (C) 2016 Leland Green... All rights reserved. 5 | Released under MIT license so you can use for any purpose. 6 | See License.md for the licensing text. 7 | 8 | **Definitions** 9 | 10 | * **Significant Changes** : Anything that makes the project commercial; non--open-source; or changes the core functionality of the project (i.e., it is no longer a security camera or video doorbell 11 | * **Commercial Software** : If you are *now making money* from the project or there is *any chance* that you will *ever make money* from the project, it is considered commercial software. 12 | * **Educational Setting** : Within the context of teaching. Not necessarily teaching about the Raspberry Pi, but teaching about any subject where this project will be useful. While every institution of formal education would certainly qualify, such an instution is *not* required. E.g., a tutor could certainly claim that they were also using within an *educational setting*. 13 | 14 | If you make **significant changes** I require only two things: **the graphic (JamPi Logo)** and **JamPi name** must be removed. You are free to use those (name and graphic) in a **non-commercial** , **or educational** setting, *only*. For a project with significant changes, you must (1) remove them from any place they are shown to the end-user and (2) state that your project was derived from the **JamPi** project in the documentation and/or the comments of the source code. In that case you must also include a link to the original project page. (This will suffice: https://github.com/lelandg/Raspberry-Pi-Security-Camera) 15 | 16 | Since the graphic is currently not used, you can just delete it, and then do not show the name **JamPi** to the *end-user* and we will be OK with each other. :-) **Simply search/replace in the file security_camera.py for "JamPi" and replace it with your product name** ; simple... especially since that's only used in the initialization log message. You are allowed to leave that in, until you make **significant changes** , when you must rename it. Fair? Thank you for your cooperation.) 17 | 18 | See *Requirements* and then *Installation* below if you want to jump right in! 19 | 20 | **Features** 21 | 22 | * This is an active project! New features are planned (no matter *when* you read this! :D ). So I hope you'll check back once-in-a-while, or follow me on Google+ or Facebook. (Everything I do is 100% visible to the public.) 23 | * Works with a Raspberry Pi camera module (AKA "RaspiCam") *OR* a USB webcam! Preferably one that has hardware H2.64 compression. 24 | * Motion detector (PIR/ePIR sensor) triggered events. When motion is detected: 25 | * Send a chat message to a list of SIP addresses. This is only done every X seconds. (Configurable; Default=1 minute) 26 | * Email and image of the intruder to a single person or to several people. Also done only ever X2 seconds. (Separate configuration for time. Default=1 minute.) 27 | * Start recording video to the SD card (default is for 15 seconds). 28 | * Flash an LED (optional) 29 | * Log on to the camera, using any Linphone client for live, streaming video from the camera. 30 | * No limit to connection time! 31 | * While connected to the live camera, the notification features are turned off. (This is to save on resources more than a lack of the Raspberry Pi, which is fully capable of doing everything simultaneously. 32 | * Only people configured (in the Python script) are allowed to connect 33 | * Optionally use as a "video doorbell". Put the button and camera (and optional motion sensor) outside the door and record a picture of everyone who rings your doorbell, plus, it rings through to your SIP videophone (Linphone is a good one), so you can see video of the person! Right now you can talk to them, but they can't talk to you. (Sound is only working one way... maybe just my system?) This is true of both the doorbell and when you make a call to this device. 34 | 35 | **Requirements** 36 | 37 | * This script requires custom hardware, which is included as a Fritzing/PNG files. Uses a PIR/ePIR sensor to detect motion. 38 | * It is recommended that you have two SIP accounts, one for the security camera device and one for you. This greatly simplifies connecting to the camera via Linphone. Simply use the "address" that is the SIP account running on the device. 39 | 40 | Sending video is resource-intensive so you should boot to Console mode (probably with auto-login if this is to be a remote deployment). To be clear, I could never get video to work when I booted my Pi to X desktop. So that is unsupported at this time. (This may be a misunderstanding of something on my part so please speak-up if you have info that may help me.) 41 | 42 | First, change to the directory with: 43 | `cd Raspberry-Pi-Security-Camera` 44 | 45 | Because it uses an I/O device you need elevated permissions to run it. So you will need to run the script with: 46 | `.startscript.sh` 47 | *or* 48 | `sudo python security_camera.py`. 49 | However, you should *always* use `.startscript.sh`. If you want something different, edit the script once and it will be changed from then on! :) 50 | 51 | **TIP** 52 | If you want to find programs in your home directory without typing the preceding `./`, whether or not you're running as sudo, you can add to your `.bashrc` with: 53 | * `nano ~/.bashrc` 54 | * Scroll to the end of the file and add the line: 55 | `export PATH=$PATH:/home/pi` 56 | * After this change you can simply type `.startscript.sh`. (And *actually* just type ".sta" and press the [tab]. It will probably type the rest of the command for you. Same thing with the other shell scripts! Save your typing for Github! :) 57 | 58 | This project also represents my first (serious) attempt at using GitHub as it was intended. You may see me open issues and then close them after a commit/merge. I hope that these comments prove useful to you. 59 | 60 | **Installation** 61 | * If you do not have git installed, run: 62 | ``` 63 | sudo apt-get install git 64 | ``` 65 | Then everyone should run: 66 | ``` 67 | git clone https://github.com/lelandg/Raspberry-Pi-Security-Camera 68 | cd Raspberry-Pi-Security-Camera 69 | ``` 70 | * After that, when updating, always run that same command to start with (or else download & unzip all files to your home directory, or whichever one you want to run from). Only the program files will be installed to this directory. In keeping with Linux standards the log will alwas be /var/log/security_camera.log 71 | * This file is owned by root so you may need to chown on it if you can't access it. I just copy to Windows and use my favorite GUI editor. 72 | * run: 73 | ``` 74 | sudo chmod +x __make_executable 75 | __make_executable 76 | ``` 77 | * This last command simply runs chmod on the three utility scripts for you. :) It is technically not required--you can run with only "sudo python security_camera.py" if you want to! But that's a mouthful, so I've made it simpler by including three scripts. (See next section.) 78 | * Install Python Linphone 79 | * Please see https://wiki.linphone.org/wiki/index.php/Raspberrypi:start for complete instructions. 80 | 81 | **Optionally** 82 | 83 | * Either install python-espeak or set "talkToEm = False" in the script. (Change the existing line, don't add one!) 84 | ``` 85 | sudo apt-get install python-espeak 86 | ``` 87 | 88 | * If you hit errors, on a Raspberry Pi, especially, please open an issue under this project. I appreciate all reports, even if it's something you do not understand! Please help me to understand what you do not understand, if at all possible. If not, just send me the log file, preferably via your DropBox (or similar service). If you need an invitation to drop box, please let me know and I will send you one. 89 | 90 | **Operation** 91 | 92 | Three shell scripts are included to make operation of the Python script a snap. They have very different names to aid you in starting, stopping and showing the process (if any) for the currently running script. 93 | 94 | You do *not* need to prefix these with "sudo". The scripts do that for you! :) These scripts are: 95 | * `.startscript.sh` -- Starts the security_camera.py after some basic "housekeeping". 96 | *This is the recommended way to run your `security_camera.py` script!* This script will: 97 | * Stop one previosly running script. (But only one!) 98 | * Remove the log file. So if you have errors you want to keep, rename `/var/log/security_camera.log` to something unique *before* you restart the script, or it will be overwritten. Or better yet, rename them into your DropBox folder and email me a link! :) 99 | * Start `security_camera.py` running in the background. 100 | * `tail -f /var/log/security_camera.log`, so you see all messages on the console. (You could disable the `tail` command if speed is an issue.) Yet they are still saved in the log file, until you restart the script. 101 | * `security_off` -- Kills the currently running script. You can run this multiple times, it won't kill anything else. (Unless the python script has exactly the same name, but then it would be the same script. Ha!) 102 | * `_showproc` -- Shows any currently running security camera Python scripts. Note that if you see only one line, you should also see a "grep" in the command portion of the output, which means that is the _showproc script, itself, *not* the Python script. So if you only see one line in the output, that means you have all instances of the security camera script stopped. 103 | 104 | **NOTES** 105 | 106 | If you see an error like this: `WARNING: ./share/sounds/linphone/rings/oldphone.wav does not exist`, you can run: 107 | ``` 108 | sudo find / -iname '*.wav' > /home/pi/soundfiles.txt && less ~/soundfiles.txt 109 | ``` 110 | to find all sound files on your machine. Once the list is displayed (and stored in `~/soundfiles.txt`, if you see a line with `/usr/local/lib/python2.7/dist-packages/linphone/share/sounds/linphone/ringback.wav`, use the parent directory as input to this command `sudo ln -s /usr/local/lib/python2.7/dist-packages/linphone/share/sounds/linphone `; such that, for this example: 111 | ``` 112 | sudo ln -s /usr/local/lib/python2.7/dist-packages/linphone/share/sounds/linphone /usr/share/sounds/linphone 113 | ``` 114 | 115 | **IMPORTANT NOTE** In your home directory you will find the file `.linphonerc` ( `~/.linphonerc` should always access it). This file has a line for `contact=`. I recommend opening this file and changing it to contain your current SIP account so that it does not conflict with that given in the script. Depending on how you've used Linphone on RPi, you may have other options that conflict! I cannot hope to document all of those, so you need to look up the documentation for these. 116 | 117 | **WARNING** Do *not* run more than one instance of the script! The shell scripts *attempt* to prevent you from doing this by calling `security_off` before it actually "turns it back on". 118 | 119 | **WARNING** Running multiple instances of the script is certainly unsupported and behavior is undefined, at best! 120 | -------------------------------------------------------------------------------- /Raspberry Pi Costing.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelandg/Raspberry-Pi-Security-Camera/978899c9d111c4d53e6fdb41efc6bca119398d80/Raspberry Pi Costing.xls -------------------------------------------------------------------------------- /__make_executable: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo chmod +x ./install_python_linphone.sh 3 | sudo chmod +x ./.startscript.sh 4 | sudo chmod +x ./security_off 5 | sudo chmod +x ./_showproc 6 | -------------------------------------------------------------------------------- /_showproc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo ps aux | grep 'python security_camera.py' 3 | -------------------------------------------------------------------------------- /doorbell2.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelandg/Raspberry-Pi-Security-Camera/978899c9d111c4d53e6fdb41efc6bca119398d80/doorbell2.ogg -------------------------------------------------------------------------------- /install_python_linphone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wget https://linphone.org/releases/linphone-python-raspberry/linphone4raspberry-3.9.0-cp27-none-any.whl 3 | sudo apt-get install python-setuptools 4 | sudo easy_install pip 5 | sudo pip install wheel 6 | sudo pip install --upgrade pip 7 | sudo pip install linphone4raspberry-3.9.0-cp27-none-any.whl 8 | 9 | -------------------------------------------------------------------------------- /jampi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lelandg/Raspberry-Pi-Security-Camera/978899c9d111c4d53e6fdb41efc6bca119398d80/jampi.png -------------------------------------------------------------------------------- /jampi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 13 | 17 | 18 | 19 | 22 | 25 | 26 | 27 | 29 | 30 | 32 | 34 | 35 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /security_camera.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Based on the "security camera" example Python script found on Linphone.org here: 3 | # https://wiki.linphone.org/wiki/index.php/Raspberrypi:start 4 | # Heavily modified to add motion detector features for Zilog ePIR SBC with ZDots. This is an advanced 5 | # version and I cannot find this exact model, so I think it's discontinued. 6 | # 7 | # Now supports PIR sensor (newer style, lower power) by simply setting PIRPIN > 0. 8 | # The default is to use pin 4 (in BCM mode = GPIO4 = physical pin = 7 = BOARD mode pin 7). 9 | # 10 | # On the off-chance that you DO have an ePIR, simply set PIRPIN = 0 and MDPIN = pin 5 on the ePIR. You'll 11 | # actually want to refer to the Fritzing/PNG file for the schematic in that case, too. In order to support 12 | # all features (at a future date), I wired up all of the ePIR pins. So it's GPIO-hungry. But I can add things 13 | # like ambient light level (which I plan to do, BTW--even if I AM the only one with the module! :) ). 14 | # 15 | # Tip: If you want to disable the LED on the camera completely, add "disable_camera_led=1" (with no quotes) to 16 | # the end of the line in /boot/cmdline.txt. *However* this is not working with Raspbian Jessie. 17 | # 18 | # Please let me know if you have questions. I will do my best to help you. (Please be patient. I am on disability.) 19 | # All modifications written by: Leland Green - aboogieman - a t - gmail.com 20 | __version__ = "0.2.1" 21 | 22 | import os 23 | import traceback 24 | import datetime 25 | import picamera 26 | import linphone # From python 27 | import pygame # pygame is only used to play sound files (in OGG format). You could easily remove all of this if needed. 28 | from email.MIMEMultipart import MIMEMultipart 29 | from email.MIMEText import MIMEText 30 | from email.MIMEImage import MIMEImage 31 | import RPi.GPIO as GPIO 32 | import io 33 | import logging 34 | # import stun 35 | import serial 36 | import signal 37 | import time 38 | import smtplib 39 | import sys 40 | 41 | # Global variables: 42 | 43 | # This is only for additional trace messages, logging some info for the outgoing call, attempting external IP, etc. 44 | DEBUG = False 45 | 46 | # Constants 47 | thisDeviceName = "rpi01" # This is used for the email subject line. This should be unique per-device! 48 | # While this is *not* required, it will let you know at a glance which camera triggered 49 | # the email! :) 50 | 51 | PRODUCTNAME = 'JamPi' # You *must* change this in any commercial setting. 52 | 53 | # WAITSECONDS also controls the shortest amount of time between printing "Motion detected". 54 | WAITSECONDS = 60 # Set to zero to send a chat message every time motion is detected. (Not advised!) 55 | 56 | # How long to wait between sending emails. Independent of WAITSECONDS. I recommend 60 or more, but use what you want. 57 | WAITEMAILSECONDS = 60 58 | 59 | RECORDVIDEO = True # If you think you want a time lapse, please try a video first. Video is *less resource intensive*! 60 | # If True, a video of length (WAITEMAILSECONDS / 2) seconds will be recorded. 61 | 62 | # Set 'disableCameraLED = True' to turn off camera LED while recording 63 | disableCameraLED = not DEBUG # This leaves the camera LED *on* when DEBUG == True. Check it out! :-) 64 | # disableCameraLED = True 65 | 66 | # Video length: default = record for 3/4 the time up until next email-event. 67 | VIDEOLENTGH = ((WAITEMAILSECONDS / 4) * 3) 68 | 69 | RECORDTIMELAPSE = False 70 | 71 | camera = 'V4L2: /dev/video0' # This *should* pick up either the RaspiCam *or* a USB web cam. :) 72 | RESOLUTION = (1296, 972) # For high-latency networks, you probably want more like this. Max is (2592, 1944) 73 | VIDEORESOLUTION = (1920, 1080) # This is not transmitted, only stored on SD card/disk. 74 | 75 | # Possible values are: 76 | # 1080p=1920x1080, uxga=1600x1200, sxga=1280x960, 720p=1280x720, xga=1024x768, 77 | # svga=800x600, 4cif=704x576, vga=640x480, cif=352x288, qvga=320x240, qcif=176x144 78 | 79 | resolutions = \ 80 | { 81 | '1080p': (1920, 1080), 82 | 'uxga': (1600, 1200), 83 | 'sxga': (1280, 960), 84 | '720p': (1280, 720), 85 | 'xga': (1024, 768), 86 | 'svga': (800, 600), 87 | '4cif': (704, 576), 88 | 'vga': (640, 480), 89 | 'cif': (352, 288), 90 | 'qvga': (320, 240), 91 | 'qcif': (176, 144) 92 | } 93 | # TODO: Implement resolution by VIDEOPHONE_RESOLUTION_BY_NAME 94 | VIDEOPHONE_RESOLUTION_BY_NAME = 'cif' # 'cif' is the default resolution. See above fore other possible choices. 95 | 96 | # Note that if you are NOT running as root, you should change this to '~/security_camera.log', or similar. 97 | LOGFILENAME = '/var/log/security_camera.log' 98 | SIPSERVER = 'sip:sip.linphone.org;transport=tls' 99 | AUTHORIZATIONDOMAN = 'sip.linphone.org' 100 | 101 | # Whether or not to save all images that are emailed to disk. (Or SD card, depending on the path you give.) 102 | SAVEEMAILEDIMAGES = True 103 | # Specify a unique mount-point here to save images to external/USB/network, etc. 104 | SAVEIMAGEDIR = '/home/pi/' # Leave this blank for current directory, else end with '/' char. 105 | 106 | # sipUserName = 'yourSipUsername' 107 | # sipPassword = 'yourSipPassword' 108 | sipUserName = 'yourEmailAddressFrom(UserNameOnly)' 109 | sipPassword = 'yoursippassword' 110 | # doorbellToAddress is the SIP (or URL) address that will be called when the "doorbell" is pressed. 111 | doorbellToAddress = 'sip:yourSipUsername@sip.linphone.org' # Who to "ring". SIP address format 112 | doorBellSoundWav = 'doorbell2.wav' # Sound for local "doorbell ring". Person pushing button hears this. 113 | doorBellSoundOgg = 'doorbell2.ogg' # Sound for local "doorbell ring". Person pushing button hears this. 114 | 115 | # sndCapture = 'ALSA: C-Media USB Headphone Set' 116 | sndCapture = 'ALSA: default device' 117 | 118 | # sndPlayback = 'snd_rpi_hifiberry_dac' 119 | sndPlayback = 'ALSA: default device' 120 | # Stuff that did NOT work for me: 121 | # 'sndrpihifiberry' # 'snd_rpi_hifiber' # 'snd_soc_hifiberry_dac' # alsa.driver_name from 'pacmd' 122 | # 'snd_rpi_hifiberry_dac' # = 'HifiBerry DAC HiFi pcm5102a-hifi-0' = 'snd_rpi_hifiberry_dac' 123 | 124 | # Now a set, not a list, which is mutable. 125 | whiteList = ('sip:yourSipUsername@sip.linphone.org', 126 | 'sip:aSecondSIPAccount@sip.linphone.org') 127 | 128 | emailFromAddress = 'yourEmailAddressFrom(UserNameOnly)' 129 | emailServer = 'smtp.gmail.com' 130 | emailPort = 587 131 | 132 | # Specify multiple destination email addresses by using this format: 133 | # emailAddressTo = ['youremail@example.com', 'anotheremail@example.com'] # Do it like this for several, 134 | 135 | # Specify a single destination email address like this: 136 | emailAddressTo = 'youremail@example.com' # or like this for just one recipient. 137 | 138 | emailAccount = 'emailaddressfrom@example.com' # Account to use for emailing 139 | emailPassword = 'youremailpassword' # Password for that account 140 | emailSubject = 'Motion detected' # Whatever you'd like in the subject line 141 | # Alternate text (which *you* may not see, but others will!): 142 | emailTextAlternate = 'Motion was detected. An image is included in the alternate MIME of this email.' 143 | 144 | # Note: This can be overridden! See a few lines down. 145 | detectMotion = True # Whether or not we have the motion detector SBC connected. True = connected. 146 | FLIPVERTICAL = False 147 | ACK = 6 # "Acknowledge" character 148 | NACK = 21 # "Non-Acknowledge" character 149 | 150 | # *** WARNING *** Do not change these unless you are SURE what you are doing! *** 151 | # *** Specifying an incorrect value for LEDPIN can *DAMAGE YOUR RASPBERRY PI*! *** 152 | # These pin numbers refer to the GPIO.BCM numbers. 153 | # The first two (LEDPIN and BUTTONPIN) can be set to zero to disable. 154 | LEDPIN = 17 # Motion-detected status - Blinking = motion detected. 0 = disable. 155 | BUTTONPIN = 18 # Button to trigger start outgoing call. 0 = disable. (E.g., for use as a "video doorbell". Thanks Toshi!) 156 | 157 | # You can--and it is recommended that YOU DO--add an additional LED by simply changing this: 158 | LEDPINDOORBELL = LEDPIN # If <> 0, this will stay on at all times, except blink when doorbell is rang. 159 | # If you use two LED's, then LEDPIN will be exclusively for motion-detection status. 160 | LEDDOORBELLBLINK = 2 # Time in seconds to blink. Blink will be ~4Hz, so at least 2 seconds is recommended. 161 | 162 | # Setting the next two to 0 (or False) will disable the motion sensor. *THIS OVERRIDES ABOVE SETTING* 163 | # MDPIN = 22 # Motion Detected pin (on ePIR). This can be any valid GPIO pin value, in BCM numbering scheme. 164 | MDPIN = 0 # Set this *AND* PIRPIN = 0 to use as a "video doorbell", only. You can do both, too, if you'd like. 165 | PIRPIN = 4 # Set this to zero to always use the MDPIN. That's for legacy ePIR devices, which you probably don't need. 166 | 167 | # Changes expected below this line. Careful changes. :) 168 | # Set GPIO for camera LED. Use 5 for Model A/B and 32 for Model B+. 169 | CAMLEDPIN = 32 # This is sporadic across models so I discourage using it. 170 | 171 | blinkCamLed = False 172 | 173 | talkToEm = True 174 | if talkToEm: 175 | from espeak import espeak 176 | 177 | 178 | # These can be changed, but beware of setting them too low because camera IO takes place during both 179 | # motion detection and sending email phases: 180 | # WARNING: THIS FUNCTION IS DEPRECATED 181 | # noinspection PyPep8Naming 182 | def readLineCR(port): 183 | s = '' 184 | while True: 185 | try: 186 | ch = port.read(1) 187 | s += ch 188 | if ch == '\r' or ch == '': 189 | break 190 | except: # TODO: Add specific exceptions here. 191 | # if traceback.print_exc() # Will be a "ready to read, but no data", in my experience. 192 | pass 193 | return s 194 | 195 | 196 | def setup_log_colors(): 197 | logging.addLevelName(logging.DEBUG, '\033[1;37m%s\033[1;0m' % logging.getLevelName(logging.DEBUG)) 198 | logging.addLevelName(logging.INFO, '\033[1;36m%s\033[1;0m' % logging.getLevelName(logging.INFO)) 199 | logging.addLevelName(logging.WARNING, '\033[1;31m%s\033[1;0m' % logging.getLevelName(logging.WARNING)) 200 | logging.addLevelName(logging.ERROR, '\033[1;41m%s\033[1;0m' % logging.getLevelName(logging.ERROR)) 201 | 202 | 203 | def setup_log(log, trace): 204 | # if log is None: 205 | setup_log_colors() 206 | fmt = "%(asctime)s.%(msecs)03d %(levelname)s: %(message)s" 207 | datefmt = "%H:%M:%S" 208 | if trace: 209 | level = logging.DEBUG 210 | else: 211 | level = logging.INFO 212 | logging.basicConfig(filename=log, level=level, format=fmt, datefmt=datefmt) 213 | 214 | 215 | def log_handler(level, msg): 216 | method = getattr(logging, level) 217 | method(msg) 218 | 219 | 220 | class SecurityCamera: 221 | def __init__(self, username='', password='', whitelist=(), camera='', snd_capture='', snd_playback=''): 222 | # type: (string, string, list, string, string, string) -> object 223 | """ 224 | Main class, params are what you'd expect based on the names. 225 | :param username: 226 | :param password: 227 | :param whitelist: 228 | :param camera: 229 | :param snd_capture: 230 | :param snd_playback: 231 | """ 232 | logging.debug("__init__") 233 | logging.info( 234 | "Initializaing {product} System version {version}...".format(product=PRODUCTNAME, version=__version__)) 235 | # logging.debug("setting audio_dscp") 236 | # Pulling my values from "Commonly used DSCP Values" table in this article: 237 | # https://en.wikipedia.org/wiki/Differentiated_services 238 | # self.core.audio_dscp = 26 239 | # self.core.video_dscp = 46 # 46 = High Priority Expedited Forwarding (EF) - TODO: Can this be lowered??? 240 | 241 | self.lastMessageTicks = time.time() # Wait one "cycle" so everything gets initialized 242 | self.lastEmailTicks = time.time() # via the TCP/IP (UDP/TLS/DTLS). 243 | 244 | self.dirname = '/var/log/jampi' 245 | if not os.path.exists(self.dirname): 246 | os.makedirs(self.dirname) 247 | 248 | # Initialize email 249 | self.smtp = smtplib.SMTP() 250 | 251 | # Initialize the motion detector. This is for the Zilog ePIR ZDot SBC. It has more features via serial mode, 252 | # so that's what we'll use here. 253 | GPIO.setwarnings(False) # Disable "this channel already in use", etc. 254 | GPIO.setmode(GPIO.BCM) 255 | # GPIO.setup(CAMLEDPIN, GPIO.OUT, initial=False) 256 | 257 | self.port = serial.Serial("/dev/ttyAMA0", baudrate=9600, timeout=2) 258 | 259 | s = "Waiting for motion sensor to come online...." 260 | print s 261 | logging.debug(s) 262 | # time.sleep(10) # Arduino example says need delays between commands for proper operation. (I suspect for 9600 bps it needs time.) 263 | time.sleep(5) 264 | 265 | self.imageDir = os.getcwd() 266 | # self.imageDir = os.path.join(os.getcwd(), 'security-images') 267 | if not os.path.exists(self.imageDir): 268 | os.makedirs(self.imageDir) 269 | 270 | self.videoDir = os.getcwd() 271 | # self.videoDir = os.path.join(os.getcwd(), 'security-videos') 272 | if not os.path.exists(self.videoDir): 273 | os.makedirs(self.videoDir) 274 | 275 | if 0 != PIRPIN: 276 | # Assume newer PIR device, signal hooked to PIRPIN 277 | logging.info("Sensor online... Turning on motion sensor...") 278 | logging.debug('calling GPIO.setup(PIRPIN, ...)') 279 | GPIO.setup(PIRPIN, GPIO.IN, GPIO.PUD_DOWN) 280 | # GPIO.add_event_detect(PIRPIN, GPIO.RISING, self.motion_detected, bouncetime=2000) # add rising edge detection on a channel 281 | print "Waiting for it to stabilize..." 282 | time.sleep(5) 283 | # while GPIO.input(PIRPIN)==1: 284 | # Current_State = 0 285 | logging.info("PIR sensor is ready.") 286 | elif 0 != MDPIN: 287 | # let the ePIR sensor wake up. 288 | # time.sleep(10) # Arduino example says need delays between commands for proper operation. (I suspect for 9600 bps it needs time.) 289 | ch = 'U' 290 | while ch == 'U': # Repeat loop if not stablized. (ePIR replies with the character 'U' until the device becomes stable) 291 | # time.sleep(1) 292 | ch = self.port.read( 293 | 1) # Sends status command to ePIR and assigns reply from ePIR to variable ch. (READ ONLY function) 294 | logging.debug('ch = %s' % (ch,)) 295 | 296 | ch = readLineCR(self.port) 297 | s = "ePIR" 298 | if PIRPIN: 299 | s = "PIR" 300 | time.sleep(1) 301 | # print "%s sensor device online..." % (s, ) 302 | 303 | self.port.write('CM') 304 | 305 | # If we don't do this, the next readLineCR() will get garbage and will take an undetermined amount of time! 306 | time.sleep(1) 307 | result = readLineCR(self.port) 308 | 309 | if len(result) > 1: result = result[-1] 310 | 311 | if result == 'R': 312 | print 'ePIR reset!' 313 | elif result == 'M' or result == 'N': 314 | print 'Motion detection mode confirmed.' 315 | else: 316 | logging.debug('Result = "%s"' % (result,)) 317 | 318 | logging.debug("ch = '%s'\r\nDevice Ready" % (ch,)) 319 | logging.info("\nePIR sensor ready.") 320 | 321 | if 0 != LEDPIN: 322 | GPIO.setup(LEDPIN, GPIO.OUT) # This light blinks when motion detected. Connect with long wires to monitor! 323 | # If the video and chat are not enough notifications. :) 324 | if 0 != LEDPINDOORBELL: 325 | GPIO.setup(LEDPINDOORBELL, GPIO.OUT) 326 | GPIO.output(LEDPINDOORBELL, 1) # Keep this LED ON. 327 | 328 | if 0 != MDPIN: 329 | GPIO.setup(MDPIN, GPIO.IN) 330 | 331 | self.doorbell_sound = None 332 | if 0 != BUTTONPIN: 333 | GPIO.setup(BUTTONPIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) 334 | 335 | # try: # Background music is loaded here. 336 | # #pygame.mixer.pre_init(48000, -16, 2, 4096) 337 | # pygame.init() 338 | # pygame.mixer.init(44100, -16, 2, 2048) 339 | # self.doorbell_sound = pygame.mixer.Sound(doorBellSoundOgg) 340 | # except pygame.error, message: 341 | # logging.error("Cannot load doorbell sound: {doorbellsound}\r\n{exception}".format( 342 | # doorbellsound=doorBellSoundOgg, exception=traceback.format_exc())) 343 | 344 | global blinkCamLed 345 | val1 = blinkCamLed # Only time we force a blinking LED is during initialization, so you know it's ready. 346 | blinkCamLed = True 347 | if LEDPINDOORBELL: 348 | self.flash_led(ledpin=LEDPINDOORBELL) 349 | else: 350 | self.flash_led() 351 | blinkCamLed = val1 352 | 353 | # Other member variables: 354 | self.imgStream = io.BytesIO() 355 | # time.sleep(2) #Allow time for ePIR warming-up 356 | 357 | self.quit = False 358 | self.username = "" 359 | self.current_call = None 360 | self.whitelist = whitelist 361 | callbacks = { 362 | 'call_state_changed': self.call_state_changed, 363 | } 364 | 365 | # Initialize & Configure the linphone core 366 | logging.basicConfig(level=logging.INFO) 367 | signal.signal(signal.SIGINT, self.signal_handler) 368 | linphone.set_log_handler(log_handler) 369 | self.core = linphone.Core.new(callbacks, None, None) 370 | self.core.max_calls = 3 371 | self.core.video_adaptive_jittcomp_enabled = False 372 | self.core.adaptive_rate_control_enabled = False 373 | # self.core.quality_reporting_enabled = False # This fails straight away. 374 | self.core.echo_cancellation_enabled = False 375 | self.core.video_capture_enabled = True 376 | self.core.video_display_enabled = False # This will only show up on a composite/HDMI monitor. 377 | self.core.keep_alive_enabled = True # This is the default at time of writing. 378 | 379 | self.core.mic_enabled = True 380 | self.core.ringback = doorBellSoundWav # This causes terrible distortion on my system 381 | # self.core.ring = doorBellSoundWav # TODO: Use a separate sound for this. 382 | 383 | tr = self.core.sip_transports 384 | # assert_equals(tr.udp_port, 5060) # default config 385 | # assert_equals(tr.tcp_port, 5060) # default config 386 | tr.udp_port = 5063 387 | tr.tcp_port = 5067 388 | tr.tls_port = 32737 389 | tr.dtls_port = 32738 390 | self.core.sip_transports = tr 391 | tr = self.core.sip_transports 392 | logging.debug('Transports = UDP: %s, TCP %s, TLS %s, DTLS %s' % \ 393 | (tr.udp_port, tr.tcp_port, tr.tls_port, tr.dtls_port)) 394 | 395 | # tr = self.core.sip_transports 396 | # tr.dtls_port = 5060 397 | # tr.tcp_port = 5061 398 | # tr.udp_port = 5062 399 | # tr.tls_port = 5063 400 | # self.core.sip_transports = tr 401 | self.core.stun_server = 'stun.linphone.org' 402 | self.core.firewall_policy = linphone.FirewallPolicy.PolicyUseIce 403 | if len(camera): 404 | self.core.video_device = camera 405 | if len(snd_capture): 406 | self.core.capture_device = snd_capture 407 | if len(snd_playback): 408 | self.core.playback_device = snd_playback 409 | 410 | # Only enable PCMU, PCMA and speex audio codecs 411 | for codec in self.core.audio_codecs: 412 | if codec.mime_type in ["PCMA", 413 | "PCMU"]: # [, "speex", "opus", "VP8", "H264", "opus", "VP8", "H264"]: # Overkill! , "SILK" 414 | self.core.enable_payload_type(codec, True) 415 | logging.info("Adding codec %s..." % (codec.mime_type,)) 416 | else: 417 | self.core.enable_payload_type(codec, False) 418 | 419 | # Only enable VP8 video codecs 420 | for codec in self.core.video_codecs: 421 | if codec.mime_type in ["VP8"]: 422 | logging.info("Adding codec %s..." % (codec.mime_type,)) 423 | self.core.enable_payload_type(codec, True) 424 | else: 425 | self.core.enable_payload_type(codec, False) 426 | 427 | logging.debug("Configuring SIP account...") 428 | self.configure_sip_account(username, password) 429 | 430 | if talkToEm: 431 | espeak.synth('Security system is now on line.') 432 | time.sleep(3) 433 | self.configured = False 434 | 435 | def captureImage(self): 436 | # Create an in-memory stream 437 | with picamera.PiCamera(sensor_mode=4) as camera: # A hack! Pin 0 causes it to not light at all. 438 | camera.led = False 439 | if FLIPVERTICAL: 440 | camera.vflip = True 441 | camera.hflip = True # No separate option, just flip both directions. 442 | # Native mode: 2592 x 1944 443 | camera.resolution = RESOLUTION 444 | camera.start_preview() 445 | # camera.framerate = 30 446 | # camera.annotate_text = "You are SO BUSTED! This image has ALREADY been emailed to security!" # Fun! :-) 447 | time.sleep(2) # Wait for the automatic gain control to settle 448 | # Now fix the values 449 | camera.shutter_speed = camera.exposure_speed 450 | camera.exposure_mode = 'off' 451 | g = camera.awb_gains 452 | camera.awb_mode = 'off' 453 | camera.awb_gains = g 454 | # Finally, take several photos with the fixed settings 455 | 456 | # Camera warm-up time 457 | # fname = '%s' + str(datetime.datetime.now()) 458 | self.imgStream.seek(0) 459 | camera.capture(self.imgStream, 'jpeg', use_video_port=True) 460 | try: 461 | outname = 'security_camera_image-{timestamp}.jpg'.format(timestamp=datetime.datetime.now().isoformat()) 462 | outf = file(outname, 'wb') 463 | self.imgStream.seek(0) 464 | outf.write(self.imgStream.read()) 465 | except: 466 | logging.error(traceback.format_exc()) 467 | camera.stop_preview() 468 | return self.imgStream 469 | 470 | def emailImage(self): 471 | logging.debug("emailImage() called") 472 | self.captureImage() 473 | 474 | # if RECORDVIDEO or RECORDTIMELAPSE: 475 | # return 476 | 477 | # Create the root message and fill in the from, to, and subject headers 478 | msgRoot = MIMEMultipart('related') 479 | msgRoot['Subject'] = 'Motion detected' 480 | msgRoot['From'] = emailFromAddress 481 | msgRoot['To'] = 'Security Team for camera ' + thisDeviceName # emailAddressTo 482 | msgRoot.preamble = 'This is a multi-part message in MIME format.' 483 | 484 | # Encapsulate the plain and HTML versions of the message body in an 485 | # 'alternative' part, so message agents can decide which they want to display. 486 | msgAlternative = MIMEMultipart('alternative') 487 | msgRoot.attach(msgAlternative) 488 | 489 | msgText = MIMEText(emailTextAlternate) 490 | msgAlternative.attach(msgText) 491 | 492 | # We reference the image in the IMG SRC attribute by the ID we give it below 493 | msgText = MIMEText( 494 | 'Alert motion detected on camera %s and here is a picture of what/whomever triggered this alert!

^^^ The Culprit ^^^' % ( 495 | thisDeviceName), 'html') 496 | msgAlternative.attach(msgText) 497 | 498 | self.imgStream.seek(0L) 499 | msgImage = MIMEImage(self.imgStream.read()) 500 | 501 | # Define the image's ID as referenced above 502 | msgImage.add_header('Content-ID', '') 503 | msgRoot.attach(msgImage) 504 | 505 | # Send the email (this example assumes SMTP authentication is required) 506 | self.smtp.connect(emailServer, emailPort) 507 | self.smtp.starttls() # TODO: Can this be moved out of the iteration? Is it expensive? 508 | self.smtp.login(emailAccount, emailPassword) 509 | self.smtp.sendmail(emailFromAddress, emailAddressTo, msgRoot.as_string()) 510 | self.smtp.quit() # TODO: Determine if this can be elsewhere, or if we can leave this open. 511 | self.lastEmailTicks = time.time() 512 | if SAVEEMAILEDIMAGES: 513 | fname = '{saveimagedir}emailedSecurityCameraImage-{date}.jpg'.format( 514 | saveimagedir=SAVEIMAGEDIR, 515 | date=datetime.datetime.now().isoformat() 516 | ) 517 | outf = file(fname, 'wb') 518 | self.imgStream.seek(0L) 519 | outf.write(self.imgStream.read()) 520 | outf.close() 521 | logging.info('Emailed and saved image to "{fname}"'.format(fname=fname)) 522 | else: 523 | logging.info('Emailed image at {currenttime}'.format(currenttime=datetime.datetime.now().isoformat())) 524 | 525 | def signal_handler(self, signal, frame): 526 | self.core.terminate_all_calls() 527 | self.quit = True 528 | 529 | def call_state_changed(self, core, call, state, message): 530 | body = "State: {state}".format(state=linphone.CallState.string(state)) 531 | if state == linphone.CallState.IncomingReceived: 532 | if call.remote_address.as_string_uri_only() in self.whitelist: 533 | params = core.create_call_params(call) 534 | params.audio_enabled = True 535 | params.audio_multicast_enabled = False 536 | params.video_multicast_enabled = False 537 | logging.debug("Call params:\r\n" "%s" % (str(params),)) 538 | core.accept_call_with_params(call, params) 539 | # call.microphone_volume_gain = 0.98 # Maximum value, I believe.... 540 | self.current_call = call 541 | logging.debug('sip_transports: dtls = %d, tcp = %d, udp = %d, tls = %d' % \ 542 | (self.core.sip_transports.dtls_port, self.core.sip_transports.tcp_port, 543 | self.core.sip_transports.udp_port, self.core.sip_transports.tls_port)) 544 | logging.debug('sip_transports_used: dtls = %d, tcp = %d, udp = %d, tls = %d' % \ 545 | (self.core.sip_transports_used.dtls_port, self.core.sip_transports_used.tcp_port, 546 | self.core.sip_transports_used.udp_port, self.core.sip_transports_used.tls_port)) 547 | if talkToEm: 548 | espeak.synth('Incoming call answered. You are being watched!') 549 | time.sleep(2) 550 | else: 551 | core.decline_call(call, linphone.Reason.Declined) 552 | for contact in self.whitelist: 553 | chat_room = core.get_chat_room_from_uri(contact) 554 | if not chat_room: 555 | continue 556 | msg = chat_room.create_message(call.remote_address_as_string + ' tried to call') 557 | chat_room.send_chat_message(msg) 558 | elif state == linphone.CallState.CallOutgoingInit \ 559 | or state == linphone.CallState.CallOutgoingProgress \ 560 | or state == linphone.CallState.CallOutgoingRinging: 561 | logging.info("\nCall remote: {address}".format(address=call.remote_address.as_string())) 562 | body += "\nFrom: {address}".format(address=call.remote_address.as_string()) 563 | 564 | elif state == linphone.CallState.End: 565 | logging.info("Call ended normally.") 566 | self.current_call = None 567 | elif state == linphone.CallState.Error: 568 | logging.error("Error ... ending call!") 569 | # core.end_call(call) 570 | self.current_call = None 571 | else: 572 | logging.debug("call_state_changed() state = %s" % (linphone.CallState.string(state),)) 573 | logging.info(body) 574 | 575 | def configure_sip_account(self, username, password): 576 | # Configure the SIP account 577 | proxy_cfg = self.core.create_proxy_config() 578 | proxy_cfg.identity_address = self.core.create_address( 579 | # 'sip:{username}@{authdomain}:5061'.format(username=username, authdomain=AUTHORIZATIONDOMAN)) 580 | 'sip:{username}@{authdomain}:5061'.format(username=username, authdomain=AUTHORIZATIONDOMAN)) 581 | # proxy_cfg.server_addr = SIPSERVER 582 | proxy_cfg.server_addr = 'sip:' + AUTHORIZATIONDOMAN 583 | proxy_cfg.register_enabled = True 584 | self.core.add_proxy_config(proxy_cfg) 585 | self.core.default_proxy_config = proxy_cfg 586 | # auth_info = self.core.create_auth_info(username, None, password, None, None, 'sip.linphone.org') 587 | auth_info = self.core.create_auth_info(username, None, password, None, None, None) 588 | self.core.add_auth_info(auth_info) 589 | logging.info('auth_info.username = {username}, '.format(username=username, authdomain=AUTHORIZATIONDOMAN)) 590 | self.username = username 591 | 592 | def run(self): 593 | while not self.quit: 594 | button_pressed = False 595 | if 0 != BUTTONPIN: 596 | button_pressed = not GPIO.input(BUTTONPIN) 597 | if button_pressed and self.core.current_call is None: 598 | # We do not check the time here. They can keep "ringing" the doorbell if they want 599 | # but it won't matter once a call is initiated. 600 | if self.doorbell_sound: 601 | self.doorbell_sound.play() 602 | try: 603 | if time.time() - self.lastMessageTicks > WAITSECONDS: 604 | self.notify_chat_contacts(message_template="Doorbell ring on %s at %s") 605 | params = self.core.create_call_params(None) 606 | params.audio_enabled = True 607 | params.video_enabled = True 608 | params.audio_multicast_enabled = False # Set these = True if you want multiple 609 | params.video_multicast_enabled = False # people to connect at once. 610 | address = linphone.Address.new(doorbellToAddress) 611 | logging.info('address = {address}, used_video_codec = {codec}'.format( 612 | address=address, 613 | codec=params.used_video_codec)) 614 | self.current_call = self.core.invite_address_with_params(address, params) 615 | if None is self.current_call: 616 | logging.error("Error creating call and inviting with params... outgoing call aborted.") 617 | if time.time() - self.lastEmailTicks >= WAITEMAILSECONDS: 618 | if LEDPINDOORBELL: 619 | self.flash_led(ledpin=LEDPINDOORBELL, stay_on=True, 620 | blink_cam_led=False, delay=0.25, blink_count=8) 621 | else: 622 | self.flash_led() 623 | self.notify_email_contacts() 624 | except KeyboardInterrupt: 625 | self.quit = True 626 | break 627 | elif detectMotion and self.core.current_call is None: 628 | motion_detected = False 629 | # Incoming calls have been handled, so check the motion detector: 630 | if 0 != PIRPIN: 631 | # motion_detected = GPIO.wait_for_edge(PIRPIN,GPIO.RISING) 632 | # motion_detected = GPIO.event_detected(PIRPIN) 633 | motion_detected = GPIO.input(PIRPIN) 634 | logging.debug("\rmotion_detected = %s, GPIO.input(PIRPIN) = %s" % ( 635 | str(motion_detected), str(GPIO.input(PIRPIN)))) 636 | elif 0 != MDPIN: 637 | motion_detected = GPIO.input(MDPIN) == 0 638 | 639 | if motion_detected: 640 | self.motion_detected() 641 | # else: 642 | # time.sleep(0.01) # 643 | self.core.iterate() 644 | 645 | def motion_detected(self): 646 | t = time.time() 647 | # logging.info('*Motion detected!*') # This may be useful when adjusting potentiometers on PIR sensor. 648 | if t - self.lastMessageTicks < WAITSECONDS and t - self.lastEmailTicks < WAITEMAILSECONDS: 649 | return 650 | logging.info('*Motion detected!*') 651 | if DEBUG: 652 | logging.debug('sip_transports: dtls = %d, tcp = %d, udp = %d, tls = %d' % \ 653 | (self.core.sip_transports.dtls_port, self.core.sip_transports.tcp_port, \ 654 | self.core.sip_transports.udp_port, self.core.sip_transports.tls_port)) 655 | logging.debug('sip_transports_used: dtls = %d, tcp = %d, udp = %d, tls = %d' % \ 656 | (self.core.sip_transports_used.dtls_port, self.core.sip_transports_used.tcp_port, \ 657 | self.core.sip_transports_used.udp_port, self.core.sip_transports_used.tls_port)) 658 | logging.debug('self.core.upnp_external_ipaddress = %s' % (self.core.upnp_external_ipaddress,)) 659 | logging.debug('self.core.nat_address = %s' % (self.core.nat_address,)) 660 | tr = self.core.sip_transports_used 661 | logging.debug( 662 | 'self.core.linphone_core_get_sip_transports_used = dtls = %d, tcp = %d, udp = %d, tls = %d' % \ 663 | (tr.dtls_port, tr.tcp_port, tr.udp_port, tr.tls_port)) 664 | 665 | if time.time() - self.lastMessageTicks > WAITSECONDS: 666 | self.notify_chat_contacts() # Note that times for message and email are independent. 667 | if time.time() - self.lastEmailTicks >= WAITEMAILSECONDS: 668 | try: 669 | self.notify_email_contacts() 670 | self.record_video_event() 671 | except KeyboardInterrupt: 672 | self.quit = True 673 | self.flash_led() 674 | 675 | def record_video_event(self): 676 | if RECORDVIDEO: 677 | with picamera.PiCamera(sensor_mode=1) as camera: 678 | if disableCameraLED: 679 | camera.led = False # The LED will still blink 680 | # What resolution for video is best??? This is full 1080p 681 | # On the Pi, the full resolution of the sensor is (2592 x 1944), and there's a 1080p video mode (1920 x 1080). 682 | # (1296, 972) # Had this 683 | camera.resolution = VIDEORESOLUTION 684 | camera.start_recording(os.path.join(self.videoDir, 'security_camera-%s.h264' % ( 685 | datetime.datetime.now().isoformat(' ')), )) 686 | camera.wait_recording(VIDEOLENTGH) 687 | camera.stop_recording() 688 | elif RECORDTIMELAPSE: 689 | with picamera.PiCamera(sensor_mode=4) as camera: # 1296 x 972, 4:3 690 | if disableCameraLED: 691 | camera.led = False 692 | camera.resolution = (1296, 972) 693 | logging.debug(('\rtime.time() - self.lastEmailTicks = %s, WAITEMAILSECONDS/2 = %d' % \ 694 | (time.time() - self.lastEmailTicks, WAITEMAILSECONDS / 2))) 695 | while time.time() - self.lastEmailTicks <= WAITEMAILSECONDS / 2: 696 | for filename in camera.capture_continuous(os.path.join(self.imageDir, 697 | 'img-%s{counter:03d}.jpg' % ( 698 | datetime.datetime.now().isoformat( 699 | ' '),))): 700 | self.core.iterate() 701 | logging.info('Captured %s' % filename) 702 | time.sleep(0.2) # wait 5 minutes 703 | 704 | def notify_email_contacts(self): 705 | self.lastEmailTicks = time.time() 706 | self.emailImage() 707 | if talkToEm: 708 | # Note we *tell them* we're going to record a video, even if we're not. Sneaky! :) 709 | espeak.synth('An image has just been emailed to security.') 710 | time.sleep(2) 711 | espeak.synth('A video is being recorded of you, even as we speak.') 712 | time.sleep(2) 713 | 714 | def notify_chat_contacts(self, message_template="'Motion detected on %s at %s'"): 715 | logging.info("Notifying contacts...") 716 | for contact in self.whitelist: 717 | logging.info("Notifying %s" % (contact,)) 718 | chat_room = self.core.get_chat_room_from_uri(contact) 719 | if not chat_room: 720 | continue 721 | dt = datetime.datetime.now() 722 | c = self.core.primary_contact_parsed 723 | ip = self.core.upnp_external_ipaddress 724 | ea = c.as_string() 725 | un = c.username 726 | cl = c.clean() 727 | # po = c.port 728 | logging.debug("c.port = {p}".format(p=c.port)) 729 | po = 5061 730 | logging.debug("dir(c) = {d}".format(d=dir(c))) 731 | logging.info("c.display_name = {cdn}, c.username = {cun}, c.port = {cp}".format( 732 | cdn=c.display_name, cun=c.username, cp=c.port)) 733 | logging.info("po = {po}, cl = {cl}, c = {c}, ip = {ip}, ea = {ea}, un = {un}".format( 734 | po=po, cl=cl, c=c, ip=ip, ea=ea, un=un)) 735 | # nattype, external_ip, external_port = stun.get_ip_info('0.0.0.0', 54320, self.core.stun_server, 3478) 736 | # logging.debug('nattype = %s, external_ip = %s, external_port = %s' % (nattype, external_ip, external_port)) 737 | # sipaddress = 'sip:yourSipUsername@%s:%s' %(external_ip, external_port) 738 | # logging.debug('sipaddress = %s' % (sipaddress, )) 739 | msg = chat_room.create_message(message_template % (ea, dt.isoformat(' '))) 740 | chat_room.send_chat_message(msg) 741 | self.lastMessageTicks = time.time() 742 | self.core.iterate() 743 | 744 | def flash_led(self, ledpin=LEDPIN, stay_on=False, blink_cam_led=blinkCamLed, delay=0.05, blink_count=10): 745 | if ledpin == LEDPINDOORBELL and not stay_on: # Goof-proof? :) 746 | logging.debug('Recursion for flash_led()...') 747 | return self.flash_led(ledpin=LEDPINDOORBELL, stay_on=True, blink_cam_led=False, delay=0.125, 748 | blink_count=int(1 / delay) * LEDDOORBELLBLINK) 749 | for j in range(0, blink_count): 750 | if ledpin: 751 | GPIO.output(ledpin, True) 752 | if blink_cam_led: 753 | GPIO.output(CAMLEDPIN, True) 754 | time.sleep(delay) 755 | if ledpin: 756 | GPIO.output(ledpin, False) 757 | if blink_cam_led: 758 | GPIO.output(CAMLEDPIN, False) 759 | time.sleep(delay) 760 | if stay_on and ledpin: 761 | GPIO.output(ledpin, True) 762 | 763 | 764 | def main(argc, argv): 765 | error = None 766 | try: 767 | setup_log(LOGFILENAME, trace=DEBUG) 768 | # These are your SIP username and password and system devices 769 | cam = SecurityCamera(username=sipUserName, password=sipPassword, whitelist=whiteList, camera=camera, 770 | snd_capture=sndCapture, snd_playback=sndPlayback) 771 | cam.run() 772 | except: 773 | traceback.print_exc() # Print the error 774 | error = traceback.format_exc() # and format to log it (if possible) 775 | finally: 776 | GPIO.cleanup() 777 | if error: 778 | logging.error(error) 779 | 780 | 781 | if __name__ == '__main__': 782 | main(len(sys.argv), sys.argv) 783 | -------------------------------------------------------------------------------- /security_off: -------------------------------------------------------------------------------- 1 | sudo kill -s KILL $(ps aux | grep 'sudo python security_camera.py' | awk -v i=1 -v j=2 'FNR == i {print $j}') 2 | sudo kill -s KILL $(ps aux | grep 'python security_camera.py' | awk -v i=1 -v j=2 'FNR == i {print $j}') 3 | cp /var/log/security_camera.log ./ 4 | 5 | --------------------------------------------------------------------------------