├── .gitignore ├── pigallery.png ├── .gitmodules ├── doc ├── teslausb_setup_variables.conf └── setup-instructions.md ├── Dockerfile.x86_64 ├── Dockerfile ├── LICENSE ├── docker-compose.yml ├── README.md └── src └── tesla_dashcam_manager.py /.gitignore: -------------------------------------------------------------------------------- 1 | temp_dir/ 2 | pigallery2/ 3 | -------------------------------------------------------------------------------- /pigallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonKagstrom/tesla_dashcam_manager/HEAD/pigallery.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tesla_dashcam"] 2 | path = tesla_dashcam 3 | url = https://github.com/ehendrix23/tesla_dashcam.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /doc/teslausb_setup_variables.conf: -------------------------------------------------------------------------------- 1 | export ARCHIVE_SYSTEM=rsync 2 | export RSYNC_USER=pi 3 | export RSYNC_SERVER=192.168.1.164 4 | export RSYNC_PATH=/mnt/staging 5 | 6 | export CAM_SIZE=30G 7 | 8 | export SSID='my-ssid' 9 | export WIFIPASS='my-password' 10 | 11 | export trigger_file_any=ARCHIVE_UPLOADED 12 | -------------------------------------------------------------------------------- /Dockerfile.x86_64: -------------------------------------------------------------------------------- 1 | FROM amd64/alpine:latest 2 | 3 | RUN apk --no-cache upgrade 4 | RUN apk add --no-cache python3 py3-pip py3-pip font-freefont ffmpeg bash py3-pillow py3-psutil py3-tzlocal py3-wheel py3-dateutil 5 | 6 | RUN pip3 install --break-system-packages staticmap 7 | 8 | RUN mkdir /usr/share/fonts/truetype 9 | RUN ln -s /usr/share/fonts/freefont/ /usr/share/fonts/truetype/freefont 10 | # Not really true, but anyway 11 | RUN ln -s /usr/share/fonts/truetype/freefont/FreeSans.otf /usr/share/fonts/truetype/freefont/FreeSans.ttf 12 | 13 | COPY src/tesla_dashcam_manager.py /usr/bin/ 14 | COPY tesla_dashcam/tesla_dashcam/tesla_dashcam.py /usr/bin/ 15 | RUN chmod +x /usr/bin/tesla*.py 16 | 17 | CMD python3 /usr/bin/tesla_dashcam_manager.py /app/staging /app/raw-storage /app/destination-path /usr/bin/tesla_dashcam.py "${TESLA_DASHCAM_ARGUMENTS}" ${RETAIN_DAYS} ${DESTINATION_RETAIN_DAYS} 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM balenalib/aarch64-alpine:3.18 2 | 3 | RUN apk --no-cache upgrade 4 | RUN apk add --no-cache python3 py3-pip py3-pip font-freefont ffmpeg bash py3-pillow py3-psutil py3-tzlocal py3-wheel py3-dateutil 5 | 6 | RUN python3 -m pip install --upgrade pip 7 | RUN pip3 install staticmap 8 | 9 | RUN mkdir /usr/share/fonts/truetype 10 | RUN ln -s /usr/share/fonts/freefont/ /usr/share/fonts/truetype/freefont 11 | # Not really true, but anyway 12 | RUN ln -s /usr/share/fonts/truetype/freefont/FreeSans.otf /usr/share/fonts/truetype/freefont/FreeSans.ttf 13 | 14 | COPY src/tesla_dashcam_manager.py /usr/bin/ 15 | COPY tesla_dashcam/tesla_dashcam/tesla_dashcam.py /usr/bin/ 16 | RUN chmod +x /usr/bin/tesla*.py 17 | 18 | CMD python3 /usr/bin/tesla_dashcam_manager.py /app/staging /app/raw-storage /app/destination-path /usr/bin/tesla_dashcam.py "${TESLA_DASHCAM_ARGUMENTS}" ${RETAIN_DAYS} ${DESTINATION_RETAIN_DAYS} 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Simon Kagstrom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/setup-instructions.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ---- 3 | 4 | Components needed 5 | ----------- 6 | - Raspberry pi zero W 7 | - >= 64GB micro SD 8 | 9 | - Raspberry pi 3/4 or similar with plenty of disk storage 10 | 11 | Downloading the teslausb image 12 | --------------------- 13 | Download the latest release of teslausb from https://github.com/marcone/teslausb 14 | 15 | Flashing the image 16 | ------------------ 17 | Raspberry pi imager 18 | 19 | Config 20 | ------ 21 | Setup rsync+ssh to your other raspberry 22 | Wifi 23 | 24 | Setup of teslausb 25 | ----------------- 26 | Plug in and wait 27 | Remount RW 28 | passwd 29 | 30 | ssh-keygen 31 | 32 | Adding an additional wifi access point 33 | -------------------------------------- 34 | wpa_supplicant.conf + /etc/network/interfaces 35 | 36 | Installing tesla dashcam 37 | ------------------------ 38 | 39 | Setup conversion of incoming clips 40 | ---------- 41 | 42 | Installing the gallery service 43 | ------------------------------ 44 | I had to edit /var/lib/pigallery2/config/xxx to get the ports correct 45 | 46 | 47 | Conclusion and other possible tasks 48 | ------ 49 | - wireguard VPN 50 | - backup 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Example snippet for docker-compose.yml. 2 | version: "3" 3 | 4 | services: 5 | 6 | ## Example setup for pigallery2, pigallery2 can be accessed at port 5000 here 7 | # pigallery2: 8 | # image: bpatrik/pigallery2:1.9.0-alpine 9 | # container_name: pigallery2 10 | # privileged: true 11 | # environment: 12 | # - NODE_ENV=production 13 | # volumes: 14 | # - "/var/lib/pigallery2/config:/app/data/config" 15 | # - "pigallery-db-data:/app/data/db" 16 | # - "/mnt/photos:/app/data/images" 17 | # - "/tmp:/app/data/tmp" 18 | # ports: 19 | # - 5000:5000 20 | # restart: always 21 | 22 | tesla-dashcam-manager: 23 | image: simonkagstrom/tesla_dashcam_manager:latest 24 | restart: always 25 | environment: 26 | - TZ=Europe/Stockholm 27 | - TESLA_DASHCAM_ARGUMENTS=--title_screen_map --text_overlay_fmt "{event_timestamp} {event_city}" 28 | - RETAIN_DAYS=365 29 | - DESTINATION_RETAIN_DAYS=0 30 | - PYTHONUNBUFFERED=1 31 | devices: 32 | - "/dev/vchiq:/dev/vchiq" 33 | volumes: 34 | - "/mnt/staging:/app/staging" 35 | - "/mnt/raw-storage:/app/raw-storage" 36 | - "/mnt/photos/TeslaCam:/app/destination-path" 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is it? 2 | Helpers for integration of [telsa_dashcam](https://github.com/ehendrix23/tesla_dashcam) + 3 | [teslausb](https://github.com/marcone/teslausb) + [pigallery2](https://github.com/bpatrik/pigallery2) 4 | for Raspberry Pi via docker. 5 | 6 | The basic idea is to setup *teslausb* to rsync files to a Raspberry Pi, use a script in 7 | this repository to process them via *tesla_dashcam* and then move them to a directory 8 | where they can be displayed via *pigallery2* through a web browser. All of this should 9 | be automated. 10 | 11 | # What is the Tesla referral code? 12 | 13 | Not that it matters anymore, but here: https://ts.la/simon17931 14 | 15 | # What do you need 16 | 17 | * A Raspberry Pi Zero W for telsausb 18 | * A Raspberry Pi 3B+/4 for the server 19 | * Good quality SD cards for both Raspberries 20 | * (Probably) an external harddisk for the server Pi 21 | 22 | # TeslaUSB instructions 23 | I use the rsync archive method with TeslaUSB to transfer clips to the server. See 24 | [doc/teslausb_setup_variables.conf](./doc/teslausb_setup_variables.conf) for my setup, 25 | and refer to [the TeslaUSB 26 | rsync instructions](https://github.com/marcone/teslausb/blob/main-dev/doc/SetupRSync.md) for information about how to setup SSH keys. 27 | 28 | # Installation instructions 29 | This repo builds a docker image that contains ffmpeg and everything needed to run 30 | tesla_dashcam ~~with GPU acceleration~~ on a Raspberry Pi 3B+, 4 or later. 31 | 32 | The easiest way to use it is to copy the snippet from [`docker-compose.yml`](./docker-compose.yml) in this 33 | repository into your own `docker-compose.yml`, where you probably already run Teslamate 34 | and pigallery2. The Raspberry Pi docker images 35 | [can be found on dockerhub](https://hub.docker.com/repository/docker/simonkagstrom/tesla_dashcam_manager/general). 36 | 37 | On the server, create `staging`, `raw-storage` and a `destination-path` to use with 38 | tesla_dashcam_manager. These are 39 | 40 | * `staging`: The path where clips comes in from Teslausb 41 | * `raw-storage`: The path where raw clips are stored for future use (removed after 42 | one year by default) 43 | * `destination-path`: Where the processed clips end up, should be readable by pigallery2 44 | 45 | After having updated your `docker-compose.yml`, do 46 | 47 | ``` 48 | # Create staging, raw-storage and destination-path 49 | mkdir -p /mnt/staging /mnt/raw-storage /mnt/photos/TeslaCam 50 | docker-compose pull 51 | docker-compose up -d 52 | ``` 53 | 54 | and you should be up and running. 55 | 56 | Note! `simonkagstrom/tesla_dashcam_manager:latest` is built for 64-bit mode (AARCH64), 57 | so make sure your Raspberry Pi is running a 64-bit OS. The last 32-bit build is 58 | `simonkagstrom/tesla_dashcam_manager:2`. 59 | 60 | # docker-compose.yml configuration options 61 | Environment variables are used to setup tesla dashcam manager, 62 | 63 | * `TZ`, the timezone to use 64 | * `TESLA_DASHCAM_ARGUMENTS`, arguments passed to tesla dashcam. 65 | * `RETAIN_DAYS`, how many days clips in the raw-storage are kept. 0 means forever 66 | * `DESTINATION_RETAIN_DAYS`, how many days processed clips are kept. 0 means forever 67 | 68 | # Screenshot of the web interface 69 | 70 | ![PiGallery2 screenshot](pigallery.png) 71 | -------------------------------------------------------------------------------- /src/tesla_dashcam_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import shutil 4 | import subprocess 5 | import os 6 | import sys 7 | import time 8 | 9 | class TeslaDashcamManager(object): 10 | 11 | def __init__(self, staging_path, raw_storage_path, destination_path, tesla_dashcam, 12 | tesla_dashcam_arguments, raw_storage_retain_days, destination_storage_retain_days): 13 | self.staging_path = staging_path 14 | self.processing_path = os.path.join(staging_path, "processing") 15 | self.work_path = os.path.join(staging_path, "work") 16 | self.raw_storage_path = raw_storage_path 17 | self.destination_path = destination_path 18 | self.tesla_dashcam = tesla_dashcam 19 | self.raw_storage_retain_days = raw_storage_retain_days 20 | self.destination_storage_retain_days = destination_storage_retain_days 21 | 22 | self.monitor_path = self.staging_path + "/ARCHIVE_UPLOADED" 23 | 24 | self.tesla_dashcam_arguments = tesla_dashcam_arguments + ["--no-check_for_update"] 25 | 26 | def move_to_raw_storage(self, dir): 27 | try: 28 | shutil.move(dir, self.raw_storage_path) 29 | except: 30 | print(f"Can't move {dir} to {self.raw_storage_path}, removing instead") 31 | shutil.rmtree(dir) 32 | 33 | def move_clips_from_staging_to_processing(self): 34 | incoming = [] 35 | if os.path.exists(self.monitor_path): 36 | os.unlink(self.monitor_path) 37 | 38 | for dirpath, dirnames, _ in os.walk(self.staging_path): 39 | for dir in dirnames: 40 | path = os.path.join(dirpath, dir) 41 | if path == self.processing_path or path == self.work_path: 42 | continue 43 | 44 | files = os.listdir(path) 45 | 46 | # If at least one file ends with .mp4, append to incoming 47 | if any(f.endswith(".mp4") for f in files): 48 | incoming.append(path) 49 | 50 | for path in incoming: 51 | dst = os.path.join(self.processing_path, os.path.basename(path)) 52 | try: 53 | shutil.move(path, dst) 54 | except: 55 | print(f"Can't move {path} to {dst}, skipping") 56 | 57 | 58 | def get_clips_from_staging(self): 59 | incoming = [] 60 | 61 | self.move_clips_from_staging_to_processing() 62 | 63 | for dirpath, dirnames, _ in os.walk(self.processing_path): 64 | for dir in dirnames: 65 | path = os.path.join(dirpath, dir) 66 | 67 | incoming.append(path) 68 | 69 | return incoming 70 | 71 | def prune_directory(self, path, retain_days, only_path): 72 | # Keep forever 73 | if retain_days == 0: 74 | return 75 | 76 | entries = os.listdir(path) 77 | 78 | now = time.time() 79 | for dir in entries: 80 | cur = os.path.join(path, dir) 81 | if cur.startswith("."): 82 | continue 83 | isdir = os.path.isdir(cur) 84 | if only_path and not isdir: 85 | continue 86 | age_days = int((now - os.path.getmtime(cur)) / (60*60*24)) 87 | if age_days >= retain_days: 88 | print(f"{cur} is {age_days} days old, removing") 89 | try: 90 | if isdir: 91 | shutil.rmtree(cur) 92 | else: 93 | os.unlink(cur) 94 | except: 95 | print(f"Can't remove {cur}") 96 | 97 | 98 | def prune_old_clips(self): 99 | self.prune_directory(self.raw_storage_path, self.raw_storage_retain_days, only_path=True) 100 | self.prune_directory(self.destination_path, self.destination_storage_retain_days, only_path=False) 101 | 102 | def process_clip(self, clip_path): 103 | args = ["python3", self.tesla_dashcam] + self.tesla_dashcam_arguments + \ 104 | ["--output", self.work_path, clip_path] 105 | try: 106 | subprocess.run(args, capture_output=True, check=True) 107 | except subprocess.CalledProcessError as exc: 108 | print(f"Error when running tesla_dashcam: RC: {exc.returncode}\n" 109 | f"Command: {exc.cmd}\n" 110 | f"Error: {exc.stderr}\n") 111 | 112 | def move_from_work_to_destination(self): 113 | 'Move clips from staging/work to the destination path' 114 | entries = os.listdir(self.work_path) 115 | for entry in entries: 116 | if not entry.endswith(".mp4"): 117 | continue 118 | 119 | p = os.path.join(self.work_path, entry) 120 | try: 121 | shutil.move(p, self.destination_path) 122 | except: 123 | print(f"Can't move {p} to {self.destination_path}") 124 | 125 | def get_and_process(self): 126 | clips = self.get_clips_from_staging() 127 | clip_count = 0 128 | if len(clips) != 0: 129 | print(f"Processing {len(clips)} clips") 130 | for clip in clips: 131 | clip_count += 1 132 | print(f"Processing clip {clip_count} of {len(clips)}") 133 | self.prune_old_clips() 134 | self.process_clip(clip) 135 | self.move_to_raw_storage(clip) 136 | if len(clips) != 0: 137 | print(f"Processed {len(clips)} clips") 138 | 139 | self.move_from_work_to_destination() 140 | 141 | def run(self): 142 | self.prune_old_clips() 143 | while True: 144 | self.get_and_process() 145 | time.sleep(2) 146 | 147 | 148 | def usage(): 149 | print(f"Usage: {__file__} [tesla_dashcam.py-path] [tesla_dashcam-arguments] [retain-days-for-raw-storage] [retain-days-for-destination-storage") 150 | sys.exit(1) 151 | 152 | def verify_create_path(path): 153 | if not os.path.exists(path): 154 | try: 155 | os.makedirs(path) 156 | except: 157 | print(f"Can't create {path}") 158 | usage() 159 | 160 | if not os.path.isdir(path): 161 | print(f"{path} is not a directory") 162 | usage() 163 | 164 | return path 165 | 166 | def split_args(s): 167 | '''Split a string into a list, but keep quoted parts as a single string''' 168 | out = [] 169 | 170 | # Probably a stupid way of doing this, and maybe it already exists? 171 | cur = "" 172 | quote = False 173 | for part in s.split(): 174 | cur = cur + part + " " 175 | if part.startswith('"'): 176 | quote = True 177 | continue 178 | elif part.endswith('"'): 179 | quote = False 180 | 181 | if not quote: 182 | out.append(cur[:-1]) 183 | cur = "" 184 | 185 | return out 186 | 187 | if __name__ == "__main__": 188 | if len(sys.argv) < 4: 189 | usage() 190 | 191 | staging_path = verify_create_path(sys.argv[1]) 192 | raw_storage_path = verify_create_path(sys.argv[2]) 193 | destination_path = verify_create_path(sys.argv[3]) 194 | 195 | verify_create_path(os.path.join(staging_path, "processing")) 196 | verify_create_path(os.path.join(staging_path, "work")) 197 | 198 | tesla_dashcam = "/usr/bin/tesla_dashcam.py" 199 | raw_storage_retain_days = 0 200 | destination_storage_retain_days = 0 201 | tesla_dashcam_arguments = [] 202 | if len(sys.argv) >= 5: 203 | tesla_dashcam = sys.argv[4] 204 | if len(sys.argv) >= 6: 205 | tesla_dashcam_arguments = split_args(sys.argv[5]) 206 | if len(sys.argv) >= 7: 207 | raw_storage_retain_days = int(sys.argv[6]) 208 | if len(sys.argv) >= 8: 209 | destination_storage_retain_days = int(sys.argv[7]) 210 | 211 | # Filter out --gpu, --gpu_type=xxx, which are no longer supported 212 | tesla_dashcam_arguments = [x for x in tesla_dashcam_arguments if not x.startswith("--gpu")] 213 | 214 | manager = TeslaDashcamManager(staging_path, raw_storage_path, destination_path, 215 | tesla_dashcam, tesla_dashcam_arguments, raw_storage_retain_days, destination_storage_retain_days) 216 | 217 | print("Waiting for trigger...") 218 | manager.run() 219 | --------------------------------------------------------------------------------