Compare commits

..

2 Commits

Author SHA1 Message Date
Mark Cotton 193464e828 bumped up EagleEyev3 version 2023-07-26 07:33:05 -06:00
Mark Cotton c4fb95c19e much better layout 2023-07-25 17:18:58 -06:00
20 changed files with 515 additions and 6208 deletions

2
.gitignore vendored
View File

@ -8,5 +8,3 @@ my_settings.py
.ipynb_checkpoints/ .ipynb_checkpoints/
.ipynb_checkpoints/* .ipynb_checkpoints/*
flask_session/ flask_session/
git-version.txt
settings.py

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Mark Cotton
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

395
Playground.ipynb Normal file
View File

@ -0,0 +1,395 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d4582341",
"metadata": {},
"source": [
"# EagleEyev3 Playground\n",
"\n",
"To make this playground work, it is easier to read the `access_token` off the filesystem but you can always run the example server `python server.py` to go thorugh the Oauth2 flow. By default it will save the `access_token` into a file named `.lazy_login`. The module looks for that file and tries reading t"
]
},
{
"cell_type": "markdown",
"id": "8355d241",
"metadata": {},
"source": [
"## Import Module"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "1394471a",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"INFO:root:Using EagleEyev3 version 0.0.4\n"
]
}
],
"source": [
"from EagleEyev3 import *\n",
"from settings import config\n",
"\n",
"logging.info(f\"Using EagleEyev3 version {EagleEyev3.__version__}\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "486a2537",
"metadata": {},
"outputs": [],
"source": [
"een = EagleEyev3(config)"
]
},
{
"cell_type": "markdown",
"id": "51b8b66e",
"metadata": {},
"source": [
"## Adjust Log Level"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "06d91db2",
"metadata": {},
"outputs": [],
"source": [
"import logging\n",
"logger = logging.getLogger()\n",
"#logger.setLevel('DEBUG')\n",
"#logger.setLevel('INFO')\n",
"logger.setLevel('WARN')\n",
"#logger.setLevel('ERROR')\n",
"#logger.setLevel('CRITICAL')"
]
},
{
"cell_type": "markdown",
"id": "0311109c-869c-4190-97c1-a6e717a8eeba",
"metadata": {},
"source": [
"## Who am I"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "8d40a4e4",
"metadata": {},
"outputs": [],
"source": [
"een.access_token = \"eyJraWQiOiI2ODYxYjBjYS0wZjI2LTExZWQtODYxZC0wMjQyYWMxMjAwMDIiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJjYWZlZGVmMiIsImF1ZCI6InZtcy5hcGkiLCJpc3MiOiJ2bXMuYXV0aC52MSIsInZtc19hY2NvdW50IjoiMDAwMjgyMDEiLCJleHAiOjE2OTA0MzM2MDMsImlhdCI6MTY4OTgyODgwMywianRpIjoiMTIzYmYwZmVmMDk0MDFlZmM0MWYxODhjODVhMGY3MTAiLCJjbGllbnRfaWQiOiIzMmRhYzMzMDY0OWI0ODI3OWY5Y2FjYzJiNmY1N2FlNSIsInZtc19jbHVzdGVyIjoiYzAxMiJ9.O3vQ4B8jRmxKIfTUzMWKVhTCecsyG3J5ARR1yrYDjero3cor1cMyVTdaUwn_pe_vWAVL4je3H2NY0nNlMHIBDbVzfIa8LqPzWfawylQN4-258gixeyjWw1vRUf99hrQXEBa4cTUvi_p7bzyARt8EPOnDczfyAx31r5z3Rt6l21kAe0uCuzCZ4W7VhbWLD9tPADBRnKb6fiS_ZA6U9DpMMWXwT1hnEBklbKOUlsi30r9AQNFGVcL_159OTde0K031LHnyRPv3LnRyXzjZpkaY9zsR--1hq_nCgNipdk42R0rePCF-hw-J0oGpj-vAsHqKadkHgy6cKsqMoE1am1kuKQ\""
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "543dae39",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'success': True,\n",
" 'response_http_status': 200,\n",
" 'data': {'httpsBaseUrl': {'hostname': 'api.c012.eagleeyenetworks.com',\n",
" 'port': 443}}}"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"een.get_base_url(cascade=True)"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "e14e2be5-a5f9-4b8c-ae60-76c61cb61b8b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Mark Cotton - mcotton@mcottondesign.com'"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"f\"{een.current_user['firstName']} {een.current_user['lastName']} - {een.current_user['email']}\""
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "4ef47ae2-a010-4b7e-87f6-3dbf0a047e16",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'eyJraWQiOiI2ODYxYjBjYS0wZjI2LTExZWQtODYxZC0wMjQyYWMxMjAwMDIiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJjYWZlZGVmMiIsImF1ZCI6InZtcy5hcGkiLCJpc3MiOiJ2bXMuYXV0aC52MSIsInZtc19hY2NvdW50IjoiMDAwMjgyMDEiLCJleHAiOjE2OTA0MzM2MDMsImlhdCI6MTY4OTgyODgwMywianRpIjoiMTIzYmYwZmVmMDk0MDFlZmM0MWYxODhjODVhMGY3MTAiLCJjbGllbnRfaWQiOiIzMmRhYzMzMDY0OWI0ODI3OWY5Y2FjYzJiNmY1N2FlNSIsInZtc19jbHVzdGVyIjoiYzAxMiJ9.O3vQ4B8jRmxKIfTUzMWKVhTCecsyG3J5ARR1yrYDjero3cor1cMyVTdaUwn_pe_vWAVL4je3H2NY0nNlMHIBDbVzfIa8LqPzWfawylQN4-258gixeyjWw1vRUf99hrQXEBa4cTUvi_p7bzyARt8EPOnDczfyAx31r5z3Rt6l21kAe0uCuzCZ4W7VhbWLD9tPADBRnKb6fiS_ZA6U9DpMMWXwT1hnEBklbKOUlsi30r9AQNFGVcL_159OTde0K031LHnyRPv3LnRyXzjZpkaY9zsR--1hq_nCgNipdk42R0rePCF-hw-J0oGpj-vAsHqKadkHgy6cKsqMoE1am1kuKQ'"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"een.access_token"
]
},
{
"cell_type": "markdown",
"id": "a22ff6c2",
"metadata": {},
"source": [
"## Get Cameras"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "bb457850",
"metadata": {},
"outputs": [],
"source": [
"ret = een.get_list_of_cameras()"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "c43f1db1",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[✅ [1001423e] - ATM & Wine,\n",
" ✅ [10090759] - Benny Camera,\n",
" ✅ [100d8666] - Cash Register,\n",
" ✅ [1002f1d0] - FR test,\n",
" ✅ [10012735] - Fuel Dock,\n",
" ✅ [100b7b3b] - Max Camera,\n",
" ✅ [10009a85] - R2D2,\n",
" ✅ [1002584c] - Safe]"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"[i for i in een.cameras if i.is_online()]"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "f4c6fe67",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[]"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"[i for i in een.cameras if not i.is_online()]"
]
},
{
"cell_type": "markdown",
"id": "8c140aaf-766f-4255-94ef-199d17cbc7a6",
"metadata": {},
"source": [
"## Getting list of Events"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "74e78ee1-33b8-4a88-9d23-cd6281603a5b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"2023-07-20T00:32:07.243-05:00 2023-07-19T18:32:07.243-05:00\n"
]
}
],
"source": [
"print(een.time_now(), een.time_before())"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "c84c30dd-4b7c-415b-8e6f-e77de70d1924",
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"for i in range(0,4):\n",
" ts = een.time_now()\n",
"\n",
" for cam in een.cameras:\n",
" blah = cam.get_list_of_events(end_timestamp=een.time_before(ts=ts, hours=(6*i)), \\\n",
" start_timestamp=een.time_before(ts=ts, hours=(6*(i+1))) )"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "13809cc7-9ec2-4e15-9495-e64feaecca6d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"5"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(een.cameras[2].events['status'])"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "67d4f79b-2b43-4bdb-9068-28ac9d8d921c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(een.cameras[0].events['motion'])"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "c7513a23",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[1008d090] Benny Camera [10090759]\n",
"[1008d090] FR test [1002f1d0]\n",
"[1008d090] Max Camera [100b7b3b]\n",
"[1008d090] R2D2 [10009a85]\n"
]
}
],
"source": [
"for cam in [i for i in een.cameras if i.bridge_id]:\n",
" print(f\"[{cam.bridge_id}] {cam.name} [{cam.id}]\")"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "8be8d503-b46d-4ba7-884e-2c21c3987129",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"FR test - 2023-07-20T01:22:47.515+00:00 - bridgeOffline \n",
"FR test - 2023-07-19T23:32:08.348+00:00 - online \n",
"R2D2 - 2023-07-19T19:38:42.286+00:00 - bridgeOffline \n",
"R2D2 - 2023-07-19T17:32:10.875+00:00 - online \n"
]
}
],
"source": [
"for cam in [i for i in een.cameras if i.bridge_id]:\n",
" for i in cam.events['status']:\n",
" print(f\"{cam.name} - {i['startTimestamp']} - {i['data'][0]['newStatus']['connectionStatus']} \")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1cebbb4e-4c4e-4ab0-9627-251eb812b2f1",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "d8a69185-54cc-4c74-9c9b-a807e601be83",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -22,10 +22,6 @@ config = {
"server_host": "127.0.0.1", "server_host": "127.0.0.1",
"server_port": "3333", "server_port": "3333",
"server_path": "login_callback", "server_path": "login_callback",
# preferences
"days_of_history": 1,
"log_level": "INFO"
} }
``` ```

264
app.py
View File

@ -15,152 +15,60 @@ from io import BytesIO
import logging import logging
logger = logging.getLogger() logger = logging.getLogger()
from functools import wraps #logger.setLevel('DEBUG')
logger.setLevel('INFO')
#logger.setLevel('WARN')
#logger.setLevel('ERROR')
#logger.setLevel('CRITICAL')
from EagleEyev3 import * from EagleEyev3 import *
from settings import config from settings import config
SECRET_KEY = "this needs to be changed to something unique and not checked into git"
# check if it could pull in a config object from settings.py
if config:
# start checking for keys in config object and set sensible defaults
if 'days_of_history' in config:
DAYS_OF_HISTORY = config['days_of_history']
else:
# fallback to a sane default
DAYS_OF_HISTORY = 1
if 'log_level' in config:
logger.setLevel(config['log_level'])
if 'client_secret' in config:
SECRET_KEY = config['client_secret']
else:
logging.setLevel(config['INFO'])
logging.info(f"Using EagleEyev3 version {EagleEyev3.__version__}") logging.info(f"Using EagleEyev3 version {EagleEyev3.__version__}")
app = Flask(__name__) app = Flask(__name__)
app.secret_key = SECRET_KEY
SESSION_TYPE = 'filesystem' SESSION_TYPE = 'filesystem'
SESSION_PERMANENT = False
PERMANENT_SESSION_LIFETIME = 60 * 60 * 24 * 7 # one week in seconds
app.config.from_object(__name__) app.config.from_object(__name__)
Session(app) Session(app)
@app.route('/')
def index():
# $ flask routes -s rule
# INFO:root:Using EagleEyev3 version 0.0.15
# Endpoint Methods Rule
# --------------------- ------- -------------------------------
# index GET /
# accounts GET /accounts
# camera_detail GET /camera/<esn>/events
# camera_detail GET /camera/<esn>/events/<int:days>
# camera_live_preivew GET /camera/<esn>/preview
# camera__preivew_image GET /camera/<esn>/preview_image
# camera_status_plot GET /camera/<esn>/status_plot
# cameras GET /cameras
# landing GET /landing
# login_callback GET /login_callback
# logout GET /logout
# static GET /static/<path:filename>
# switch_account GET /switch_account
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
logging.debug(session)
if 'een' in session and session['een'].access_token:
logging.debug(f"@login_required access_token: {session['een'].access_token}")
return f(*args, **kwargs)
else:
# failed to find a valid session so redirecting to landing page
if 'een' in session:
logging.debug(f"@login_required access_token: {session['een'].access_token}")
else:
logging.warn('@login_requried failed to find a valid session')
return redirect(url_for('landing'))
return decorated_function
@app.route('/landing')
def landing():
# sometimes you just want to get to the landing page and want to avoid the index redirect logic
if 'een' in session: if 'een' in session:
een = session['een'] een = session['een']
else: else:
een = EagleEyev3(config) een = EagleEyev3(config)
session['een'] = een session['een'] = een
base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize"
path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}"
values = { "login_link": f"{base_url}{path_url}" }
return render_template('cover.html', template_values=values)
@app.route('/')
@login_required
def index():
# get een instance from session, @login_required already checked for it
een = session['een']
# using current_user as a proxy for an established valid session # using current_user as a proxy for an established valid session
if een.access_token == None: if een.access_token == None:
base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize" base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize"
path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}" path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}"
values = { "login_link": f"{base_url}{path_url}" } return redirect(f"{base_url}{path_url}")
return render_template('cover.html', template_values=values)
# call this to check if session is actually valid # call this to check if session is actually valid
check = een.get_current_user() check = een.get_current_user()
if 'success' not in check or check['success'] == False: if 'success' not in check or check['success'] == False:
base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize" base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize"
path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}" path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}"
return redirect(f"{base_url}{path_url}")
values = { "login_link": f"{base_url}{path_url}" }
return render_template('cover.html', template_values=values)
else: else:
logging.info(f"{check['success']} - check get_current_user {een.current_user['email']} {een.access_token}") logging.info(f"{check['success']} - check get_current_user {een.current_user['email']} {een.access_token}")
#logging.info(een.cameras)
# this call could get expensive
een.get_list_of_cameras() een.get_list_of_cameras()
een.get_list_of_accounts()
if len(een.accounts) > 0 and een.active_account == None: #logging.info(een.cameras)
# they need to pick and account
logging.info(f"redirecting to accounts.html because they don't have an active account { een.active_account }")
values = {
"current_user": een.current_user,
"accounts": een.accounts,
"active_account": een.active_account
}
redirect(url_for('accounts'))
values = { values = {
"current_user": een.current_user, "current_user": een.current_user,
"cameras": een.cameras, "cameras": een.cameras
"camera_count": len(een.cameras),
"camera_count_online": len([i for i in een.cameras if i.is_online()]),
"camera_count_offline": len([i for i in een.cameras if i.is_offline()]),
"accounts": een.accounts,
"active_account": een.active_account
} }
return render_template('index.html', template_values=values) return render_template('index.html', template_values=values)
@ -168,34 +76,28 @@ def index():
@app.route('/login_callback') @app.route('/login_callback')
def login_callback(): def login_callback():
# This is getting the ?code= querystring value from the HTTP request. # This is getting the ?code= querystring value from the HTTP request.
code = request.args.get('code') code = request.args.get('code')
# create a new een object and store it in their session if 'een' in session:
een = EagleEyev3(config) een = session['een']
session['een'] = een else:
een = EagleEyev3(config)
if (code): if (code):
# use the include code parameter to complete login process # use the include code parameter to complete login process
oauth_object = een.login_tokens(code) oauth_object = een.login_tokens(code)
logging.debug(oauth_object)
# let's try resaving this to see if it fixs the missing access_token on first request after login
session['een'] = een
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/logout') @app.route('/logout')
def logout(): def logout():
if 'een' in session:
# logout isn't working APIv3 right now so don't wait for the call to fail before logging out een = session['een']
# if 'een' in session: een.logout()
# een = session['een']
# een.logout()
session.pop('een') session.pop('een')
@ -203,68 +105,28 @@ def logout():
@app.route('/cameras') @app.route('/cameras')
@login_required
def cameras(): def cameras():
if 'een' in session:
een = session['een']
else:
een = EagleEyev3(config)
# get een instance from session, @login_required already checked for it een.get_list_of_cameras()
een = session['een']
logging.debug(een.get_list_of_cameras())
values = { values = {
"current_user": een.current_user, "current_user": een.current_user,
"cameras": een.cameras, "cameras": een.cameras
"camera_count": len(een.cameras),
"camera_count_online": len([i for i in een.cameras if i.is_online()]),
"camera_count_offline": len([i for i in een.cameras if i.is_offline()])
} }
return render_template('cameras_partial.html', template_values=values) return render_template('cameras_partial.html', template_values=values)
@app.route('/accounts')
@login_required
def accounts():
# get een instance from session, @login_required already checked for it
een = session['een']
logging.debug(een.get_list_of_accounts())
values = {
"current_user": een.current_user,
"accounts": een.accounts,
"active_account": een.active_account
}
return render_template('accounts_partial.html', template_values=values)
@app.route('/switch_account')
@login_required
def switch_account():
# This is getting the ?account= querystring value from the HTTP request.
account = request.args.get('account')
# get een instance from session, @login_required already checked for it
een = session['een']
if (account):
# switch into account
logging.info(f"attempting to switch into account {account}")
logging.debug(een.login_from_reseller(target_account_id=account))
return redirect(url_for('index'))
@app.route('/camera/<esn>/preview_image') @app.route('/camera/<esn>/preview_image')
@login_required
def camera__preivew_image(esn=None): def camera__preivew_image(esn=None):
if 'een' in session:
# get een instance from session, @login_required already checked for it een = session['een']
een = session['een'] else:
een = EagleEyev3(config)
camera = een.get_camera_by_id(esn) camera = een.get_camera_by_id(esn)
res = camera.get_live_preview() res = camera.get_live_preview()
@ -274,13 +136,12 @@ def camera__preivew_image(esn=None):
else: else:
return send_file('static/placeholder.png') return send_file('static/placeholder.png')
@app.route('/camera/<esn>/preview') @app.route('/camera/<esn>/preview')
@login_required
def camera_live_preivew(esn=None): def camera_live_preivew(esn=None):
if 'een' in session:
# get een instance from session, @login_required already checked for it een = session['een']
een = session['een'] else:
een = EagleEyev3(config)
camera = een.get_camera_by_id(esn) camera = een.get_camera_by_id(esn)
@ -292,20 +153,20 @@ def camera_live_preivew(esn=None):
return render_template('camera_preview.html', template_values=values) return render_template('camera_preview.html', template_values=values)
@app.route("/camera/<esn>/events/<int:days>")
@app.route('/camera/<esn>/events')
@login_required
def camera_detail(esn=None, days=DAYS_OF_HISTORY):
# get een instance from session, @login_required already checked for it @app.route('/camera/<esn>/events')
een = session['een'] def camera_detail(esn=None):
if 'een' in session:
een = session['een']
else:
een = EagleEyev3(config)
camera = een.get_camera_by_id(esn) camera = een.get_camera_by_id(esn)
now = een.time_now()
# because of API limitation, can only query 24 hours max for i in tqdm(range(0,4)):
for i in tqdm(range(0, days)): camera.get_list_of_events(end_timestamp=een.time_before(ts=now, hours=6*i), \
camera.get_list_of_events(end_timestamp=een.time_before(hours=24*i), \ start_timestamp=een.time_before(ts=now, hours=6*(i+1)))
start_timestamp=een.time_before(hours=24*(i+1)))
values = { values = {
"current_user": een.current_user, "current_user": een.current_user,
@ -315,34 +176,25 @@ def camera_detail(esn=None, days=DAYS_OF_HISTORY):
return render_template('camera_events_partial.html', template_values=values) return render_template('camera_events_partial.html', template_values=values)
@app.route('/camera/<esn>/status_plot') @app.route('/camera/<esn>/status_plot')
@login_required
def camera_status_plot(esn=None): def camera_status_plot(esn=None):
if 'een' in session:
# get een instance from session, @login_required already checked for it een = session['een']
een = session['een'] else:
een = EagleEyev3(config)
cam = een.get_camera_by_id(esn) cam = een.get_camera_by_id(esn)
logging.debug(cam.events['status'][0])
if cam.events['status'][0]['endTimestamp'] == None:
logging.debug('found empty end_timestamp')
cam.events['status'][0]['endTimestamp'] = str(pd.Timestamp.utcnow())
atm_df = pd.DataFrame(cam.events['status'][::-1], columns=['id', 'startTimestamp', 'actorId', 'data']) atm_df = pd.DataFrame(cam.events['status'][::-1], columns=['id', 'startTimestamp', 'actorId', 'data'])
atm_df['ts'] = pd.to_datetime(atm_df.startTimestamp) atm_df['ts'] = pd.to_datetime(atm_df.startTimestamp)
atm_df['status_desc'] = atm_df['data'].apply(lambda x: x[0]['newStatus']['connectionStatus']) atm_df['status_desc'] = atm_df['data'].apply(lambda x: x[0]['newStatus']['connectionStatus'])
atm_df['status'] = atm_df['status_desc'].replace(to_replace=['online', 'offline', 'error', 'deviceOffline', 'deviceOnline', 'off', 'bridgeOffline', 'unknown'], value=[1,0,0,0,0,0,0,0]) atm_df['status'] = atm_df['status_desc'].replace(to_replace=['online', 'offline', 'error', 'deviceOffline', 'deviceOnline', 'off', 'bridgeOffline'], value=[1,0,0,0,0,0,0])
imp = atm_df.set_index(['ts']) imp = atm_df.set_index(['ts'])
imp['startTimestamp'] = pd.to_datetime(imp['startTimestamp']) imp['startTimestamp'] = pd.to_datetime(imp['startTimestamp'])
imp = imp.drop(['id', 'actorId', 'data', 'status_desc'], axis=1) imp = imp.drop(['id', 'actorId', 'data', 'status_desc'], axis=1)
imp['status'] = imp['status'].ffill() data = imp.resample('S').bfill()
data = imp.resample('S').ffill() data['status'] = data['status'].astype('int64')
# logging.debug(data.tail(200))
# data['status'] = data['status'].astype('int64')
data = data.drop(['startTimestamp'], axis=1) data = data.drop(['startTimestamp'], axis=1)
@ -362,18 +214,14 @@ def camera_status_plot(esn=None):
# Save it to a temporary buffer. # Save it to a temporary buffer.
buf = BytesIO() buf = BytesIO()
fig.savefig(buf, format="png", transparent=True) fig.savefig(buf, format="png")
# Embed the result in the html output. # Embed the result in the html output.
data = base64.b64encode(buf.getbuffer()).decode("ascii") data = base64.b64encode(buf.getbuffer()).decode("ascii")
return f"<h3>Graph of Status Events <i class='bi bi-bar-chart'></i></h3><div class='col-md-12'><img src='data:image/png;base64,{data}' style='max-width:100%'/></div>" return f"<div class='col-md-12'><img src='data:image/png;base64,{data}' style='max-width:100%'/></div>"
if __name__ == '__main__': if __name__ == '__main__':
app.run(host=config.server_host, port=config.server_port) app.run(host=een.server_host, port=een.server_port)

View File

@ -14,7 +14,7 @@ Werkzeug==2.3.6
gunicorn==20.1.0 gunicorn==20.1.0
cachelib==0.10.2 cachelib==0.10.2
Flask-Session==0.5.0 Flask-Session==0.5.0
EagleEyev3>=0.0.18 EagleEyev3>=0.0.8
tqdm tqdm
pandas pandas
numpy numpy

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,106 +0,0 @@
/*
* Globals
*/
/* Links */
a,
a:focus,
a:hover {
color: #fff;
}
/* Custom default button */
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:focus {
color: #333;
text-shadow: none; /* Prevent inheritance from `body` */
background-color: #fff;
border: .05rem solid #fff;
}
/*
* Base structure
*/
html,
body {
height: 100%;
background-color: #333;
}
body {
display: -ms-flexbox;
display: flex;
color: #fff;
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
}
.cover-container {
max-width: 42em;
}
/*
* Header
*/
.masthead {
margin-bottom: 2rem;
}
.masthead-brand {
margin-bottom: 0;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, .5);
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255, 255, 255, .25);
}
.nav-masthead .nav-link + .nav-link {
margin-left: 1rem;
}
.nav-masthead .active {
color: #fff;
border-bottom-color: #fff;
}
@media (min-width: 48em) {
.masthead-brand {
float: left;
}
.nav-masthead {
float: right;
}
}
/*
* Cover
*/
.cover {
padding: 0 1.5rem;
}
.cover .btn-lg {
padding: .75rem 1.25rem;
font-weight: 700;
}
/*
* Footer
*/
.mastfoot {
color: rgba(255, 255, 255, .5);
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
<div class="row">
<div class="col-4 offset-5">
<h2>
{% for account in template_values['accounts'] %}
{% if account.id == template_values['active_account'] %}
{{ account.name }}
{% endif %}
{% endfor %}
</h2>
</div>
<div class="col-3">
<label >Choose Account:</label>
<select onchange="this.options[this.selectedIndex].value && (window.location = this.options[this.selectedIndex].value);">
{% for account in template_values['accounts'] %}
<option value="/switch_account?account={{ account.id }}"
{% if account.id == template_values['active_account'] %}
selected
{% endif %}
title="{{ account.id }}">
{{ account.name }}
</option>
{% endfor %}
</select>
</div>
</div>

View File

@ -26,22 +26,6 @@
<!-- self-hosted fontawesome --> <!-- self-hosted fontawesome -->
<link href="/static/css/all.css" rel="stylesheet"> <link href="/static/css/all.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link href="/static/theme.css" rel="stylesheet">
<style>
.row {
border: dashed lightgray 1px;
}
</style>
<style>
.left_text {
text-align: left;
}
</style>
{% block style %} {% block style %}
{% endblock %} {% endblock %}
@ -59,7 +43,7 @@
<ul class="navbar-nav mr-auto mt-2 mt-lg-0"> <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
<li class="nav-item active"> <li class="nav-item active">
<a href="/" class="navbar-brand d-flex align-items-center"> <a href="/" class="navbar-brand d-flex align-items-center">
<small>status</small>.mcotton.space <strong><small>status</small>.mcotton.space</strong>
</a> </a>
</li> </li>

View File

@ -1,3 +1,3 @@
<h3>Preview Image <i class="bi bi-card-image"></i></h3> <h3>Preview Image</h3>
<h5>{{ template_values['camera'].name }}</h5> <h5>{{ template_values['camera'].name }}</h5>
<img src="/camera/{{ template_values['camera'].id }}/live_preview" style="max-height:360px;"> <img src="/camera/{{ template_values['camera'].id }}/live_preview" style="max-height:360px;">

View File

@ -1,46 +1,15 @@
<h3>List of Events <i class="bi bi-calendar-event"></i></h3> <h3>Events</h3>
<h5>{{ template_values['camera'].name }}</h5> <h5>{{ template_values['camera'].name }}</h5>
<a href="/camera/{{ template_values['camera'].id }}/events/3" hx-get="/camera/{{ template_values['camera'].id }}/events/3" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress"> <ul>
<button class="btn btn-outline-success"> {% if template_values['events'] %}
3x<i class="bi bi-calendar-event" title="click to load events list for 3 days"></i> {% for event in template_values['events'] %}
</button> <li>
</a> {{ event['data'][0]['newStatus']['connectionStatus'] }}
<a href="/camera/{{ template_values['camera'].id }}/events/7" hx-get="/camera/{{ template_values['camera'].id }}/events/7" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress"> <br> <small>{{ event['endTimestamp'] }}</small>
<button class="btn btn-outline-success"> <br> <small>{{ event['startTimestamp'] }}</small>
7x<i class="bi bi-calendar-event" title="click to load events list for 7 days"></i> </li>
</button> {% endfor %}
</a> {% else %}
<a href="/camera/{{ template_values['camera'].id }}/events/14" hx-get="/camera/{{ template_values['camera'].id }}/events/14" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress"> <li>No events in the last six hours</li>
<button class="btn btn-outline-success"> {% endif %}
14x<i class="bi bi-calendar-event" title="click to load events list for 14 days"></i> </ul>
</button>
</a>
<a href="/camera/{{ template_values['camera'].id }}/status_plot" hx-get="/camera/{{ template_values['camera'].id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot" hx-indicator=".progress">
<button class="btn btn-outline-success">
Graph Events <i class="bi bi-bar-chart" title="click to generate graph of events"></i>
</button>
</a>
{% if template_values['events'] %}
{% for event in template_values['events'] %}
<div class="row">
<div class="col-2 left_text">
<small>Status:</small>
</div>
<div class="col-9 offset-1 left_text">
<b>{{ event['data'][0]['newStatus']['connectionStatus'] }}</b>
</div>
<div class="col-2 left_text">
<small>End:</small>
</div>
<div class="col-9 offset-1 left_text">
{{ event['endTimestamp'] }}
</div>
<div class="col-2 left_text">
<small>Start:</small>
</div>
<div class="col-9 offset-1 left_text">
{{ event['startTimestamp'] }}
</div>
</div>
{% endfor %}
{% endif %}

View File

@ -1,3 +1,3 @@
<h3>Preview Image <i class="bi bi-card-image"></i></h3> <h3>Preview Image</h3>
<h5>{{ template_values['camera'].name }}</h5> <h5>{{ template_values['camera'].name }}</h5>
<img src="/camera/{{ template_values['camera'].id }}/preview_image" style="max-height:360px; max-width:100%"> <img src="/camera/{{ template_values['camera'].id }}/preview_image" style="max-height:360px;">

View File

@ -1,61 +1,47 @@
<h3>Cameras <i class="bi bi-camera-video"></i></h3>
<h5>Online <small>[{{ template_values['camera_count_online'] }} of {{ template_values['camera_count'] }}]</small></h5> <h3>Cameras</h3>
<h5>Online</h5>
<ul>
{% for camera in template_values['cameras'] %} {% for camera in template_values['cameras'] %}
{% if camera.is_online() %} {% if camera.is_online() and camera.bridge_id %}
<div class="row"> <li>{{ camera.name }}</li>
<div class="col-sm-5 col-md-6 offset-1 left_text" style="overflow: hidden;"> <a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail">preview</a>
<span title="ESN: {{ camera.id }}">{{ camera.name }}</span> <a href="/camera/{{ camera.id }}/event" hx-get="/camera/{{ camera.id }}/events" hx-trigger="click" hx-target="#camera_status_events">events</a>
</div> <a href="/camera/{{ camera.id }}/status_plot" hx-get="/camera/{{ camera.id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot">graph</a>
<div class="col-sm-6 col-md-5">
<a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail" hx-indicator=".progress">
<button class="btn btn-outline-success">
<i class="bi bi-card-image" title="click to load preview image"></i>
</button>
</a>
<a href="/camera/{{ camera.id }}/events" hx-get="/camera/{{ camera.id }}/events" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress">
<button class="btn btn-outline-success">
<i class="bi bi-calendar-event" title="click to load events list"></i>
</button>
</a>
<!-- <a href="/camera/{{ camera.id }}/status_plot" hx-get="/camera/{{ camera.id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot" hx-indicator=".progress">
<button class="btn btn-outline-success">
<i class="bi bi-bar-chart" title="click to generate graph of events"></i>
</button>
</a> -->
</div>
</div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<hr>
<h5>Offline <small>[{{ template_values['camera_count_offline'] }} of {{ template_values['camera_count'] }}]</small></h5>
{% for camera in template_values['cameras'] %} {% for camera in template_values['cameras'] %}
{% if camera.is_offline() %} {% if camera.is_online() and camera.bridge_id == None %}
<div class="row"> <li>{{ camera.name }}</li>
<div class="col-sm-5 col-md-6 offset-1 left_text" style="overflow: hidden;"> <a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail">preview</a>
<span title="ESN: {{ camera.id }} Status: {{ camera.status['connectionStatus'] }}">{{ camera.name }}</span> <a href="/camera/{{ camera.id }}/event" hx-get="/camera/{{ camera.id }}/events" hx-trigger="click" hx-target="#camera_status_events">events</a>
</div> <a href="/camera/{{ camera.id }}/status_plot" hx-get="/camera/{{ camera.id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot">graph</a>
<div class="col-sm-6 col-md-5">
<a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail" hx-indicator=".progress">
<button class="btn btn-outline-success">
<i class="bi bi-card-image" title="click to load preview image"></i>
</button>
</a>
<a href="/camera/{{ camera.id }}/events" hx-get="/camera/{{ camera.id }}/events" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress">
<button class="btn btn-outline-success">
<i class="bi bi-calendar-event" title="click to load events list"></i>
</button>
</a>
<!-- <a href="/camera/{{ camera.id }}/status_plot" hx-get="/camera/{{ camera.id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot" hx-indicator=".progress">
<button class="btn btn-outline-success">
<i class="bi bi-bar-chart" title="click to generate graph of events"></i>
</button>
</a> -->
</div>
</div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul>
<h5>Offline</h5>
<ul>
{% for camera in template_values['cameras'] %}
{% if camera.is_offline() and camera.bridge_id %}
<li>{{ camera.name }}</li>
<a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail">preview</a>
<a href="/camera/{{ camera.id }}/event" hx-get="/camera/{{ camera.id }}/events" hx-trigger="click" hx-target="#camera_status_events">events</a>
<a href="/camera/{{ camera.id }}/status_plot" hx-get="/camera/{{ camera.id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot">graph</a>
{% endif %}
{% endfor %}
<hr>
{% for camera in template_values['cameras'] %}
{% if camera.is_offline() and camera.bridge_id == None %}
<li>{{ camera.name }}</li>
<a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail">preview</a>
<a href="/camera/{{ camera.id }}/event" hx-get="/camera/{{ camera.id }}/events" hx-trigger="click" hx-target="#camera_status_events">events</a>
<a href="/camera/{{ camera.id }}/status_plot" hx-get="/camera/{{ camera.id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot">graph</a>
{% endif %}
{% endfor %}
</ul>
<button hx-get="/cameras" hx-trigger="click" hx-target="#camera_list">load cameras</button>
<button hx-get="/cameras" hx-trigger="click" hx-target="#camera_list" class="btn btn-md btn-outline-success" hx-indicator=".progress">refresh <i class="bi bi-arrow-clockwise"></i></button>
<br>

View File

@ -1,61 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="/static/favicon.ico">
<title>status.mcotton.space</title>
<!-- Bootstrap core CSS -->
<link href="/static/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="/static/cover.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand"><a href="/"><small>status</small>.mcotton.space</a></h3>
<!-- <nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Contact</a>
</nav>
</div> -->
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Camera Status History</h1>
<p class="lead">Tired of losing your arms due to offline cameras? Tired of doughnuts being stolen from your breakfast plate? Wish you could see a graph of beeps and boops? Today is your lucky day.</p>
<p class="lead">
<a href="{{ template_values['login_link'] }}" class="btn btn-lg btn-secondary">Get Started</a>
</p>
<hr>
<video src="/static/cover_movie_2.mp4" style="max-width:100%;" autoplay controls loop muted preload >
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p>This project is open-source and can be found at <a href="https://git.mcotton.space/mcotton/EE-status-v3">EE-status-v3</a> and uses <a href="https://pypi.org/project/EagleEyev3/">EagleEyev3</a>.</p>
</div>
</footer>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery-slim.min.js"><\/script>')</script>
<script src="../../assets/js/vendor/popper.min.js"></script>
<script src="/static/bootstrap.min.js"></script>
</body>
</html>

View File

@ -12,110 +12,6 @@
overflow: auto; overflow: auto;
} }
.progress {
position: fixed;
top: 0;
z-index: 1000;
height: 4px;
width: 100%;
border-radius: 2px;
background-clip: padding-box;
overflow: hidden;
}
.progress .indeterminate:before {
content: "";
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395)
infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
}
.progress .indeterminate:after {
content: "";
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
infinite;
-webkit-animation-delay: 1.15s;
animation-delay: 1.15s;
}
.progress {
display: none;
}
.htmx-request .progress {
display: inline;
}
.htmx-request.progress {
display: inline;
}
@-webkit-keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@keyframes indeterminate {
0% {
left: -35%;
right: 100%;
}
60% {
left: 100%;
right: -90%;
}
100% {
left: 100%;
right: -90%;
}
}
@-webkit-keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
@keyframes indeterminate-short {
0% {
left: -200%;
right: 100%;
}
60% {
left: 107%;
right: -8%;
}
100% {
left: 107%;
right: -8%;
}
}
</style> </style>
{% endblock %} {% endblock %}
@ -126,14 +22,6 @@
{% block main %} {% block main %}
{% if template_values['accounts']|count > 1 %}
{% include 'accounts_partial.html' %}
{% endif %}
<div class="progress" style="height: 3px; background-color: white;">
<div class="indeterminate" style="background-color: red;"></div>
</div>
<div class="container text-center"> <div class="container text-center">
<div class="row"> <div class="row">
@ -141,16 +29,16 @@
{% include 'cameras_partial.html' %} {% include 'cameras_partial.html' %}
</div> </div>
<div class="col-md-7" id="camera_detail"> <div class="col-md-7" id="camera_detail">
<h2>Preview Image <i class="bi bi-card-image"></i></h2> <h2>Preview Image</h2>
</div> </div>
</div> </div>
<br> <br>
<div class="row"> <div class="row">
<div class="col-md-5" id="camera_status_events" style="max-height:400px; overflow:auto;"> <div class="col-md-5" id="camera_status_events" style="max-height:400px; overflow:auto;">
<h2>List of Events <i class="bi bi-calendar-event"></i></h2> <h2>List of Events</h2>
</div> </div>
<div class="col-md-7" id="camera_status_plot"> <div class="col-md-7" id="camera_status_plot">
<h2>Graph of Status Events <i class="bi bi-bar-chart"></i></h2> <h2>Graph of Status Events</h2>
</div> </div>
</div> </div>