#! /usr/bin/env python3 import argparse import sys import logging import getpass import asyncio import exif from datetime import datetime from blinkpy.blinkpy import Blink from blinkpy.auth import Auth from blinkpy.helpers.util import json_load from pathlib import Path logger = logging.getLogger(__name__) def decdeg2dms(dd): mult = -1 if dd < 0 else 1 mnt,sec = divmod(abs(dd)*3600, 60) deg,mnt = divmod(mnt, 60) return mult*deg, mult*mnt, mult*sec async def main() -> int: # Handle program arguments ap = argparse.ArgumentParser( prog="blink-fetch", description="Fetch an image from a Blink camera" ) ap.add_argument("--username", help="Blink username", required=True) ap.add_argument("--password", help="Blink password") ap.add_argument("--camera-id", help="Camera ID", default="155295") ap.add_argument("--output-dir", help="Output directory", default="/tmp/blink") ap.add_argument("--no-2fa", help="Don't try to get 2FA credentials", action="store_true") ap.add_argument("--no-exif", help="Don't write EXIF data", action="store_true") ap.add_argument("--exif-camera", help="Camera name", default="Blink Mini") ap.add_argument("--exif-latitude", "--exif-lat", help="Camera latitude (Decimal Degrees)") ap.add_argument("--exif-longitude", "--exif-lng", help="Camera longitude (Decimal Degrees)") ap.add_argument( "-v", "--verbose", help="Enable verbose logging", action="store_true" ) args = ap.parse_args() # Configure logging logging.basicConfig( level=logging.DEBUG if args.verbose else logging.INFO, format="%(levelname)s: %(message)s", ) # Ask for the password if it wasn't provided if args.password is None: args.password = getpass.getpass(prompt="Blink Password: ") # Authenticate with Blink servers auth = Auth({"username": args.username, "password": args.password}, no_prompt=args.no_2fa) blink = Blink() blink.auth = auth await blink.start() # Find the requested camera for name, camera in blink.cameras.items(): logger.debug(f"Found camera: {name} ({camera.attributes['camera_id']})") if camera.attributes["camera_id"] == args.camera_id: logger.info("Found requested camera") break else: logger.error("Could not find requested camera") return 1 # Fetch the image logger.info("Fetching image") await camera.snap_picture() await blink.refresh() # Create the output directory if it doesn't exist now = datetime.now() out_file = Path( f"{args.output_dir}/camera_{args.camera_id}.{now.strftime('%Y%m%d_%H%M%S')}.jpg" ) out_file.parent.mkdir(parents=True, exist_ok=True) logger.info(f"Writing image to: {out_file}") await camera.image_to_file(str(out_file)) # Handle EXIF data if not args.no_exif: logger.info("Re-reading image to inject EXIF data") with open(out_file, "rb") as f: image = exif.Image(f) # Set the camera type image.model = args.exif_camera # If the user provided a latitude and longitude, set it # if args.exif_latitude and args.exif_longitude: # image.gps_latitude = decdeg2dms(float(args.exif_latitude)) # image.gps_longitude = decdeg2dms(float(args.exif_longitude)) # image.gps_latitude_ref = "N" # image.gps_longitude_ref = "W" # Set the timestamp image.datetime_original = now.strftime(exif.DATETIME_STR_FORMAT) # Write the EXIF data back to the file logger.info("Writing EXIF data") with open(out_file, "wb") as f: f.write(image.get_file()) return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))