Compare commits
46 Commits
Author | SHA1 | Date |
---|---|---|
Mark Cotton | cc4d33d91e | |
Mark Cotton | 6891d581a3 | |
Mark Cotton | a7d6911489 | |
Mark Cotton | e2056290c9 | |
Mark Cotton | 316ea642d9 | |
Ubuntu | a91b48d260 | |
Mark Cotton | e4274e3ea2 | |
Mark Cotton | ba7ecb3282 | |
Mark Cotton | 516340f2b4 | |
Mark Cotton | 5d736db324 | |
Mark Cotton | 094b7b754f | |
Mark Cotton | b02ba38eea | |
Mark Cotton | fb10657e60 | |
Ubuntu | a00347c7ee | |
Mark Cotton | e4dbb9c8e1 | |
Ubuntu | 03cc8797bc | |
Ubuntu | 3b9b0795ff | |
Mark Cotton | dff7923892 | |
Mark Cotton | 24b6a36700 | |
Mark Cotton | 2b78898f0b | |
Mark Cotton | 55b3b82983 | |
Mark Cotton | 014b767265 | |
Mark Cotton | 9067a2a6ce | |
Mark Cotton | 939553b4a5 | |
Mark Cotton | 132addb162 | |
Mark Cotton | 28d5a853f3 | |
Mark Cotton | 382a3859cb | |
Mark Cotton | f0a7366f31 | |
Mark Cotton | 3183e109f7 | |
Mark Cotton | 22bec51d81 | |
Mark Cotton | a44090f042 | |
Mark Cotton | 95fbcfd2d4 | |
Mark Cotton | d10789f002 | |
Mark Cotton | cd5db12a2f | |
Mark Cotton | 9a46f72a51 | |
Mark Cotton | dd77dec94a | |
Mark Cotton | 81eb4daca3 | |
Mark Cotton | 53ae5aa8f1 | |
Mark Cotton | 499e7fa836 | |
Mark Cotton | 67817f5094 | |
Mark Cotton | 6a2923801f | |
Mark Cotton | 032563c85d | |
Mark Cotton | 294dc70774 | |
Mark Cotton | f20787b16c | |
Mark Cotton | 1abb81cb4f | |
Mark Cotton | 557cf2c681 |
|
@ -8,3 +8,5 @@ my_settings.py
|
|||
.ipynb_checkpoints/
|
||||
.ipynb_checkpoints/*
|
||||
flask_session/
|
||||
git-version.txt
|
||||
settings.py
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
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
395
Playground.ipynb
|
@ -1,395 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
|
@ -22,6 +22,10 @@ config = {
|
|||
"server_host": "127.0.0.1",
|
||||
"server_port": "3333",
|
||||
"server_path": "login_callback",
|
||||
|
||||
# preferences
|
||||
"days_of_history": 1,
|
||||
"log_level": "INFO"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
258
app.py
258
app.py
|
@ -15,60 +15,152 @@ from io import BytesIO
|
|||
import logging
|
||||
logger = logging.getLogger()
|
||||
|
||||
#logger.setLevel('DEBUG')
|
||||
logger.setLevel('INFO')
|
||||
#logger.setLevel('WARN')
|
||||
#logger.setLevel('ERROR')
|
||||
#logger.setLevel('CRITICAL')
|
||||
from functools import wraps
|
||||
|
||||
from EagleEyev3 import *
|
||||
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__}")
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.secret_key = SECRET_KEY
|
||||
SESSION_TYPE = 'filesystem'
|
||||
SESSION_PERMANENT = False
|
||||
PERMANENT_SESSION_LIFETIME = 60 * 60 * 24 * 7 # one week in seconds
|
||||
app.config.from_object(__name__)
|
||||
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:
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3(config)
|
||||
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
|
||||
if een.access_token == None:
|
||||
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}"
|
||||
return redirect(f"{base_url}{path_url}")
|
||||
values = { "login_link": f"{base_url}{path_url}" }
|
||||
return render_template('cover.html', template_values=values)
|
||||
|
||||
# call this to check if session is actually valid
|
||||
check = een.get_current_user()
|
||||
if 'success' not in check or check['success'] == False:
|
||||
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}"
|
||||
return redirect(f"{base_url}{path_url}")
|
||||
|
||||
values = { "login_link": f"{base_url}{path_url}" }
|
||||
return render_template('cover.html', template_values=values)
|
||||
else:
|
||||
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_accounts()
|
||||
|
||||
#logging.info(een.cameras)
|
||||
if len(een.accounts) > 0 and een.active_account == None:
|
||||
# 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,
|
||||
"cameras": een.cameras
|
||||
"accounts": een.accounts,
|
||||
"active_account": een.active_account
|
||||
}
|
||||
redirect(url_for('accounts'))
|
||||
|
||||
values = {
|
||||
"current_user": een.current_user,
|
||||
"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)
|
||||
|
@ -76,28 +168,34 @@ def index():
|
|||
|
||||
@app.route('/login_callback')
|
||||
def login_callback():
|
||||
|
||||
# This is getting the ?code= querystring value from the HTTP request.
|
||||
code = request.args.get('code')
|
||||
|
||||
if 'een' in session:
|
||||
een = session['een']
|
||||
else:
|
||||
# create a new een object and store it in their session
|
||||
een = EagleEyev3(config)
|
||||
session['een'] = een
|
||||
|
||||
|
||||
if (code):
|
||||
# use the include code parameter to complete login process
|
||||
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'))
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
if 'een' in session:
|
||||
een = session['een']
|
||||
een.logout()
|
||||
|
||||
# logout isn't working APIv3 right now so don't wait for the call to fail before logging out
|
||||
# if 'een' in session:
|
||||
# een = session['een']
|
||||
# een.logout()
|
||||
|
||||
session.pop('een')
|
||||
|
||||
|
@ -105,28 +203,68 @@ def logout():
|
|||
|
||||
|
||||
@app.route('/cameras')
|
||||
@login_required
|
||||
def cameras():
|
||||
if 'een' in session:
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3(config)
|
||||
|
||||
een.get_list_of_cameras()
|
||||
# get een instance from session, @login_required already checked for it
|
||||
een = session['een']
|
||||
|
||||
logging.debug(een.get_list_of_cameras())
|
||||
|
||||
values = {
|
||||
"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)
|
||||
|
||||
|
||||
@app.route('/camera/<esn>/preview_image')
|
||||
def camera__preivew_image(esn=None):
|
||||
if 'een' in session:
|
||||
@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')
|
||||
@login_required
|
||||
def camera__preivew_image(esn=None):
|
||||
|
||||
# get een instance from session, @login_required already checked for it
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3(config)
|
||||
|
||||
camera = een.get_camera_by_id(esn)
|
||||
res = camera.get_live_preview()
|
||||
|
@ -136,12 +274,13 @@ def camera__preivew_image(esn=None):
|
|||
else:
|
||||
return send_file('static/placeholder.png')
|
||||
|
||||
|
||||
@app.route('/camera/<esn>/preview')
|
||||
@login_required
|
||||
def camera_live_preivew(esn=None):
|
||||
if 'een' in session:
|
||||
|
||||
# get een instance from session, @login_required already checked for it
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3(config)
|
||||
|
||||
camera = een.get_camera_by_id(esn)
|
||||
|
||||
|
@ -153,20 +292,20 @@ def camera_live_preivew(esn=None):
|
|||
|
||||
return render_template('camera_preview.html', template_values=values)
|
||||
|
||||
|
||||
@app.route("/camera/<esn>/events/<int:days>")
|
||||
@app.route('/camera/<esn>/events')
|
||||
def camera_detail(esn=None):
|
||||
if 'een' in session:
|
||||
@login_required
|
||||
def camera_detail(esn=None, days=DAYS_OF_HISTORY):
|
||||
|
||||
# get een instance from session, @login_required already checked for it
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3(config)
|
||||
|
||||
camera = een.get_camera_by_id(esn)
|
||||
now = een.time_now()
|
||||
|
||||
for i in tqdm(range(0,4)):
|
||||
camera.get_list_of_events(end_timestamp=een.time_before(ts=now, hours=6*i), \
|
||||
start_timestamp=een.time_before(ts=now, hours=6*(i+1)))
|
||||
# because of API limitation, can only query 24 hours max
|
||||
for i in tqdm(range(0, days)):
|
||||
camera.get_list_of_events(end_timestamp=een.time_before(hours=24*i), \
|
||||
start_timestamp=een.time_before(hours=24*(i+1)))
|
||||
|
||||
values = {
|
||||
"current_user": een.current_user,
|
||||
|
@ -176,25 +315,34 @@ def camera_detail(esn=None):
|
|||
|
||||
return render_template('camera_events_partial.html', template_values=values)
|
||||
|
||||
|
||||
@app.route('/camera/<esn>/status_plot')
|
||||
@login_required
|
||||
def camera_status_plot(esn=None):
|
||||
if 'een' in session:
|
||||
|
||||
# get een instance from session, @login_required already checked for it
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3(config)
|
||||
|
||||
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['ts'] = pd.to_datetime(atm_df.startTimestamp)
|
||||
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'], value=[1,0,0,0,0,0,0])
|
||||
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])
|
||||
imp = atm_df.set_index(['ts'])
|
||||
|
||||
imp['startTimestamp'] = pd.to_datetime(imp['startTimestamp'])
|
||||
imp = imp.drop(['id', 'actorId', 'data', 'status_desc'], axis=1)
|
||||
data = imp.resample('S').bfill()
|
||||
data['status'] = data['status'].astype('int64')
|
||||
imp['status'] = imp['status'].ffill()
|
||||
data = imp.resample('S').ffill()
|
||||
# logging.debug(data.tail(200))
|
||||
# data['status'] = data['status'].astype('int64')
|
||||
|
||||
data = data.drop(['startTimestamp'], axis=1)
|
||||
|
||||
|
@ -214,14 +362,18 @@ def camera_status_plot(esn=None):
|
|||
|
||||
# Save it to a temporary buffer.
|
||||
buf = BytesIO()
|
||||
fig.savefig(buf, format="png")
|
||||
fig.savefig(buf, format="png", transparent=True)
|
||||
# Embed the result in the html output.
|
||||
data = base64.b64encode(buf.getbuffer()).decode("ascii")
|
||||
return f"<div class='col-md-12'><img src='data:image/png;base64,{data}' style='max-width:100%'/></div>"
|
||||
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>"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=een.server_host, port=een.server_port)
|
||||
app.run(host=config.server_host, port=config.server_port)
|
||||
|
|
|
@ -14,7 +14,7 @@ Werkzeug==2.3.6
|
|||
gunicorn==20.1.0
|
||||
cachelib==0.10.2
|
||||
Flask-Session==0.5.0
|
||||
EagleEyev3>=0.0.8
|
||||
EagleEyev3>=0.0.18
|
||||
tqdm
|
||||
pandas
|
||||
numpy
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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
|
@ -0,0 +1,27 @@
|
|||
|
||||
|
||||
<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>
|
|
@ -26,6 +26,22 @@
|
|||
<!-- self-hosted fontawesome -->
|
||||
<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 %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -43,7 +59,7 @@
|
|||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||
<li class="nav-item active">
|
||||
<a href="/" class="navbar-brand d-flex align-items-center">
|
||||
<strong><small>status</small>.mcotton.space</strong>
|
||||
<small>status</small>.mcotton.space
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<h3>Preview Image</h3>
|
||||
<h3>Preview Image <i class="bi bi-card-image"></i></h3>
|
||||
<h5>{{ template_values['camera'].name }}</h5>
|
||||
<img src="/camera/{{ template_values['camera'].id }}/live_preview" style="max-height:360px;">
|
||||
|
|
|
@ -1,15 +1,46 @@
|
|||
<h3>Events</h3>
|
||||
<h3>List of Events <i class="bi bi-calendar-event"></i></h3>
|
||||
<h5>{{ template_values['camera'].name }}</h5>
|
||||
<ul>
|
||||
{% if template_values['events'] %}
|
||||
<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">
|
||||
<button class="btn btn-outline-success">
|
||||
3x<i class="bi bi-calendar-event" title="click to load events list for 3 days"></i>
|
||||
</button>
|
||||
</a>
|
||||
<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">
|
||||
<button class="btn btn-outline-success">
|
||||
7x<i class="bi bi-calendar-event" title="click to load events list for 7 days"></i>
|
||||
</button>
|
||||
</a>
|
||||
<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">
|
||||
<button class="btn btn-outline-success">
|
||||
14x<i class="bi bi-calendar-event" title="click to load events list for 14 days"></i>
|
||||
</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'] %}
|
||||
<li>
|
||||
{{ event['data'][0]['newStatus']['connectionStatus'] }}
|
||||
<br> <small>{{ event['endTimestamp'] }}</small>
|
||||
<br> <small>{{ event['startTimestamp'] }}</small>
|
||||
</li>
|
||||
<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 %}
|
||||
{% else %}
|
||||
<li>No events in the last six hours</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
|
@ -1,3 +1,3 @@
|
|||
<h3>Preview Image</h3>
|
||||
<h3>Preview Image <i class="bi bi-card-image"></i></h3>
|
||||
<h5>{{ template_values['camera'].name }}</h5>
|
||||
<img src="/camera/{{ template_values['camera'].id }}/preview_image" style="max-height:360px;">
|
||||
<img src="/camera/{{ template_values['camera'].id }}/preview_image" style="max-height:360px; max-width:100%">
|
||||
|
|
|
@ -1,47 +1,61 @@
|
|||
|
||||
|
||||
<h3>Cameras</h3>
|
||||
<h5>Online</h5>
|
||||
<ul>
|
||||
<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>
|
||||
{% for camera in template_values['cameras'] %}
|
||||
{% if camera.is_online() 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>
|
||||
{% if camera.is_online() %}
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-6 offset-1 left_text" style="overflow: hidden;">
|
||||
<span title="ESN: {{ camera.id }}">{{ camera.name }}</span>
|
||||
</div>
|
||||
<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 %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
|
||||
<h5>Offline <small>[{{ template_values['camera_count_offline'] }} of {{ template_values['camera_count'] }}]</small></h5>
|
||||
{% for camera in template_values['cameras'] %}
|
||||
{% if camera.is_online() 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>
|
||||
{% if camera.is_offline() %}
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-6 offset-1 left_text" style="overflow: hidden;">
|
||||
<span title="ESN: {{ camera.id }} Status: {{ camera.status['connectionStatus'] }}">{{ camera.name }}</span>
|
||||
</div>
|
||||
<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 %}
|
||||
{% 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>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<!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>
|
|
@ -12,6 +12,110 @@
|
|||
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>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -22,6 +126,14 @@
|
|||
|
||||
{% 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="row">
|
||||
|
@ -29,16 +141,16 @@
|
|||
{% include 'cameras_partial.html' %}
|
||||
</div>
|
||||
<div class="col-md-7" id="camera_detail">
|
||||
<h2>Preview Image</h2>
|
||||
<h2>Preview Image <i class="bi bi-card-image"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-md-5" id="camera_status_events" style="max-height:400px; overflow:auto;">
|
||||
<h2>List of Events</h2>
|
||||
<h2>List of Events <i class="bi bi-calendar-event"></i></h2>
|
||||
</div>
|
||||
<div class="col-md-7" id="camera_status_plot">
|
||||
<h2>Graph of Status Events</h2>
|
||||
<h2>Graph of Status Events <i class="bi bi-bar-chart"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Reference in New Issue