Compare commits
No commits in common. "main" and "full_example" have entirely different histories.
main
...
full_examp
|
@ -8,5 +8,3 @@ my_settings.py
|
||||||
.ipynb_checkpoints/
|
.ipynb_checkpoints/
|
||||||
.ipynb_checkpoints/*
|
.ipynb_checkpoints/*
|
||||||
flask_session/
|
flask_session/
|
||||||
git-version.txt
|
|
||||||
settings.py
|
|
||||||
|
|
15
Dockerfile
15
Dockerfile
|
@ -1,15 +0,0 @@
|
||||||
|
|
||||||
FROM python:3.9-slim
|
|
||||||
|
|
||||||
COPY ./requirements.txt /app/requirements.txt
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apt update
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip
|
|
||||||
|
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
|
|
||||||
|
|
||||||
CMD [ "./startup.sh" ]
|
|
|
@ -0,0 +1,641 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from .settings import *
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pytz import timezone
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class EagleEyev3():
|
||||||
|
"""
|
||||||
|
Class representing the EagleEyev3 client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Initializes the EagleEyev3 client object.
|
||||||
|
"""
|
||||||
|
self.client_id = None
|
||||||
|
self.client_secret = None
|
||||||
|
self.access_token = None
|
||||||
|
self.refresh_token = None
|
||||||
|
self.redirect_uri = None
|
||||||
|
|
||||||
|
self._load_vars_from_settings()
|
||||||
|
|
||||||
|
self.user_base_url = None
|
||||||
|
self.current_user = None
|
||||||
|
self.users = []
|
||||||
|
self.bridges = []
|
||||||
|
self.cameras = []
|
||||||
|
self.switches = []
|
||||||
|
self.users = []
|
||||||
|
self.accounts = []
|
||||||
|
self.user_tz_obj = None
|
||||||
|
|
||||||
|
self.lazy_login = True
|
||||||
|
|
||||||
|
if self.lazy_login:
|
||||||
|
try:
|
||||||
|
self._load_access_token()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logging.warn("self.lazy_login is set to {self.lazy_login} but could not find .lazy_login file to load")
|
||||||
|
|
||||||
|
def _load_vars_from_settings(self):
|
||||||
|
"""
|
||||||
|
Load variables from the settings module.
|
||||||
|
"""
|
||||||
|
self.client_id = settings.client_id
|
||||||
|
self.client_secret = settings.client_secret
|
||||||
|
self.server_protocol = settings.server_protocol
|
||||||
|
self.server_host = settings.server_host
|
||||||
|
self.server_port = settings.server_port
|
||||||
|
self.server_path = settings.server_path
|
||||||
|
|
||||||
|
# Combine server_protocol, server_host, and server_port to make the redirect_uri
|
||||||
|
# Note: Please see the note in settings.py about trailing slashes and modify this line if needed
|
||||||
|
|
||||||
|
self.redirect_uri = f"{self.server_protocol}://{self.server_host}:{self.server_port}/{self.server_path}"
|
||||||
|
|
||||||
|
|
||||||
|
def _save_access_token(self):
|
||||||
|
with open(".lazy_login", "w") as json_file:
|
||||||
|
json.dump({
|
||||||
|
'access_token': self.access_token,
|
||||||
|
'refresh_token': self.refresh_token,
|
||||||
|
'current_user': self.current_user
|
||||||
|
}, json_file)
|
||||||
|
|
||||||
|
def _load_access_token(self):
|
||||||
|
with open(".lazy_login", "r") as json_file:
|
||||||
|
saved = json.load(json_file)
|
||||||
|
if 'access_token' in saved:
|
||||||
|
self.access_token = saved['access_token']
|
||||||
|
if 'refresh_token' in saved:
|
||||||
|
self.refresh_token = saved['refresh_token']
|
||||||
|
|
||||||
|
self.get_base_url(cascade=True)
|
||||||
|
|
||||||
|
|
||||||
|
def time_now(self):
|
||||||
|
return datetime.now(tz=self.user_tz_obj).isoformat(timespec='milliseconds')
|
||||||
|
|
||||||
|
|
||||||
|
def time_before(self, ts=None, hours=6):
|
||||||
|
if ts == None:
|
||||||
|
ts = datetime.now(tz=self.user_tz_obj)
|
||||||
|
|
||||||
|
if type(ts) == str:
|
||||||
|
ts = datetime.fromisoformat(ts)
|
||||||
|
|
||||||
|
return (ts - timedelta(hours=hours)).isoformat(timespec='milliseconds')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def login_tokens(self, code=None, cascade=True):
|
||||||
|
"""
|
||||||
|
Obtains login tokens using the authorization code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code (str): The authorization code.
|
||||||
|
cascade (bool): Indicates whether to cascade and get the base URL and current user information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, data, and current user information.
|
||||||
|
"""
|
||||||
|
baseUrl = "https://auth.eagleeyenetworks.com/oauth2/token"
|
||||||
|
pathUrl = f"?grant_type=authorization_code&scope=vms.all&code={code}&redirect_uri={self.redirect_uri}" # Note the trailing slash, make sure it matches the whitelist
|
||||||
|
url = baseUrl + pathUrl
|
||||||
|
|
||||||
|
# Send a POST request to obtain login tokens
|
||||||
|
response = requests.post(url, auth=(self.client_id, self.client_secret))
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in login_tokens")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
self.access_token = response_json['access_token']
|
||||||
|
self.refresh_token = response_json['refresh_token']
|
||||||
|
|
||||||
|
if self.lazy_login:
|
||||||
|
self._save_access_token()
|
||||||
|
|
||||||
|
if cascade:
|
||||||
|
self.get_base_url()
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json,
|
||||||
|
'current_user': self.current_user
|
||||||
|
}
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
"""
|
||||||
|
Revokes token.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = "https://auth.eagleeyenetworks.com/oauth2/revoke"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"token": self.access_token,
|
||||||
|
"token_type_hint": "access_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send a POST request to obtain the base URL
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in logout")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
logging.info(f"call to logout: {response_json}")
|
||||||
|
|
||||||
|
self.access_token = None
|
||||||
|
self.refresh_token = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_base_url(self, cascade=True):
|
||||||
|
"""
|
||||||
|
Obtains the base URL for the user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cascade (bool): Indicates whether to cascade and get the current user information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = "https://api.eagleeyenetworks.com/api/v3.0/clientSettings"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send a GET request to obtain the base URL
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_base_url")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
if 'httpsBaseUrl' in response_json and 'hostname' in response_json['httpsBaseUrl']:
|
||||||
|
self.user_base_url = response_json['httpsBaseUrl']['hostname']
|
||||||
|
if cascade:
|
||||||
|
self.get_current_user()
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_current_user(self):
|
||||||
|
"""
|
||||||
|
Obtains the information of the current user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/users/self?include=timeZone"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send a GET request to obtain the current user information
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_current_user")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
self.current_user = response_json
|
||||||
|
self.user_tz_obj = timezone(response_json['timeZone']['timeZone'])
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_list_of_users(self):
|
||||||
|
"""
|
||||||
|
Obtains the list of users.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/users?include=timeZone"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_users")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
self.users = [i for i in response_json['results']]
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_list_of_cameras(self):
|
||||||
|
"""
|
||||||
|
Obtains the list of cameras.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/cameras?include=status"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_cameras")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
self.cameras = [
|
||||||
|
Camera(id=i['id'],\
|
||||||
|
name=i['name'],\
|
||||||
|
status=i['status'],\
|
||||||
|
account_id=i['accountId'],\
|
||||||
|
bridge_id=i['bridgeId'],\
|
||||||
|
user_base_url=self.user_base_url,\
|
||||||
|
een_instance=self)
|
||||||
|
for i in response_json['results']]
|
||||||
|
for camera in self.cameras:
|
||||||
|
camera.user_base_url = self.user_base_url
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_list_of_bridges(self):
|
||||||
|
"""
|
||||||
|
Obtains the list of bridges.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/bridges"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_bridges")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
self.bridges = [i for i in response_json['results']]
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_list_of_switches(self):
|
||||||
|
"""
|
||||||
|
Obtains the list of switches.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/switches"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_switches")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
self.switches = [i for i in response_json['results']]
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_list_of_available_devices(self, deviceType__in="camera"):
|
||||||
|
"""
|
||||||
|
Obtains the list of available devices.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/availableDevices?deviceType__in={deviceType__in}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_available_devices")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_list_of_multi_cameras(self):
|
||||||
|
"""
|
||||||
|
Obtains the list of multi-cameras.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/multiCameras"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_multi_cameras")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_list_of_feeds(self):
|
||||||
|
"""
|
||||||
|
Obtains the list of feeds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/feeds"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_feeds")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_camera_by_id(self, esn):
|
||||||
|
found_camera = None
|
||||||
|
for camera in self.cameras:
|
||||||
|
if camera.id == esn:
|
||||||
|
found_camera = camera
|
||||||
|
break
|
||||||
|
|
||||||
|
if found_camera == None:
|
||||||
|
camera = Camera()
|
||||||
|
|
||||||
|
logging.debug(f"returning camera {camera} for search query {esn}")
|
||||||
|
return camera
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Device():
|
||||||
|
|
||||||
|
def __init__(self, id=None, name=None, status=None, account_id=None, user_base_url=None, een_instance=None):
|
||||||
|
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.status = status
|
||||||
|
self.account_id = account_id
|
||||||
|
self.user_base_url = user_base_url,
|
||||||
|
self.een_instance = een_instance
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
if 'connectionStatus' in self.status:
|
||||||
|
return self.status['connectionStatus']
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_online(self):
|
||||||
|
if 'connectionStatus' in self.status:
|
||||||
|
return self.status['connectionStatus'] == "online"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_offline(self):
|
||||||
|
if 'connectionStatus' in self.status:
|
||||||
|
return self.status['connectionStatus'] == "deviceOffline" \
|
||||||
|
or self.status['connectionStatus'] == "bridgeOffline" \
|
||||||
|
or self.status['connectionStatus'] == "error"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.is_online():
|
||||||
|
online = '✅'
|
||||||
|
elif self.is_offline():
|
||||||
|
online = '❌'
|
||||||
|
else:
|
||||||
|
online = '?'
|
||||||
|
return f"{online} [{self.id}] - {self.name}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Camera(Device):
|
||||||
|
|
||||||
|
def __init__(self, id=None, name=None, status=None, account_id=None, bridge_id=None, user_base_url=None, een_instance=None):
|
||||||
|
super().__init__(id=id, name=name, status=status, account_id=account_id, user_base_url=user_base_url, een_instance=een_instance)
|
||||||
|
self.bridge_id = bridge_id
|
||||||
|
self.previews = []
|
||||||
|
self.videos = []
|
||||||
|
self.events = {
|
||||||
|
'status': [],
|
||||||
|
'motion': []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_list_of_events(self, start_timestamp=None, end_timestamp=None):
|
||||||
|
"""
|
||||||
|
Obtains the list of events.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary containing the success status, response HTTP status code, and data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if start_timestamp == None or end_timestamp == None:
|
||||||
|
logging.debug(f"get_list_of_events called without timestamp")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"response_http_status": None,
|
||||||
|
"data": { 'msg': 'get_list_of_events called without required args, needs start_timestamp, end_timestamp' }
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/events?pageSize=100&include=een.deviceCloudStatusUpdate.v1&startTimestamp__gte={start_timestamp}&endTimestamp__lte={end_timestamp}&actor=camera%3A{self.id}&type__in=een.deviceCloudStatusUpdateEvent.v1"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.een_instance.access_token}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers, timeout=(3, 5))
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
logging.debug(f"{response.status_code} returned from {url} with {headers} and {response.text}")
|
||||||
|
logging.info(f"{response.status_code} in get_list_of_events")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
success = True
|
||||||
|
# filter events by type
|
||||||
|
[self.events['status'].append(i) for i in response.json()['results'] if i['type'] == 'een.deviceCloudStatusUpdateEvent.v1']
|
||||||
|
[self.events['motion'].append(i) for i in response.json()['results'] if i['type'] == 'een.motionDetectionEvent.v1']
|
||||||
|
|
||||||
|
# remove duplicates
|
||||||
|
seen = set()
|
||||||
|
self.events['status'] = [event for event in self.events['status'] if event['endTimestamp'] and event['id'] not in seen and not seen.add(event['id'])]
|
||||||
|
seen = set()
|
||||||
|
self.events['motion'] = [event for event in self.events['motion'] if event['id'] not in seen and not seen.add(event['id'])]
|
||||||
|
|
||||||
|
# sort by event startTimestamp descending
|
||||||
|
self.events['status'] = sorted(self.events['status'], key=lambda x: x['startTimestamp'], reverse=True)
|
||||||
|
self.events['motion'] = sorted(self.events['motion'], key=lambda x: x['startTimestamp'], reverse=True)
|
||||||
|
else:
|
||||||
|
success = False
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logging.warn(f"timeout expired for {self.id} get_llist_of_events()")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"response_http_status": 0,
|
||||||
|
"data": None
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.warn(e)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"response_http_status": 0,
|
||||||
|
"data": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": success,
|
||||||
|
"response_http_status": response.status_code,
|
||||||
|
"data": response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_live_preview(self):
|
||||||
|
|
||||||
|
url = f"https://{self.user_base_url}/api/v3.0/media/liveImage.jpeg?deviceId={self.id}&type=preview"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.een_instance.access_token}",
|
||||||
|
"Accept": "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers, timeout=(3, 5))
|
||||||
|
logging.info(f"{response.status_code} in get_live_preview")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logging.warn(f"timeout expired for {self.id} get_live_preview()")
|
||||||
|
response = None
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.warn(e)
|
||||||
|
response = None
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
# Set up your application and get client id/secrete first
|
||||||
|
# https://developerv3.eagleeyenetworks.com/page/my-application
|
||||||
|
client_id = ""
|
||||||
|
client_secret = ""
|
||||||
|
|
||||||
|
# you will need to add approved redirect_uris in your application
|
||||||
|
# this examples assumes you've added http://127.0.0.1:3333/login_callback
|
||||||
|
# change the following variables if you did something different
|
||||||
|
# Note: do not use localhost for server_host, use 127.0.0.1 instead
|
||||||
|
server_protocol = "http"
|
||||||
|
server_host = "127.0.0.1"
|
||||||
|
server_port = "3333"
|
||||||
|
server_path = "login_callback"
|
21
LICENSE
21
LICENSE
|
@ -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.
|
|
|
@ -0,0 +1,459 @@
|
||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"source": [
|
||||||
|
"from EagleEyev3 import EagleEyev3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "486a2537",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stderr",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"INFO:root:200 in get_base_url\n",
|
||||||
|
"INFO:root:200 in get_current_user\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"een = EagleEyev3()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": 4,
|
||||||
|
"id": "e14e2be5-a5f9-4b8c-ae60-76c61cb61b8b",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"'Mark Cotton - mcotton@mcottondesign.com'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 4,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"f\"{een.current_user['firstName']} {een.current_user['lastName']} - {een.current_user['email']}\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"id": "4ef47ae2-a010-4b7e-87f6-3dbf0a047e16",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"'eyJraWQiOiI2ODYxYjBjYS0wZjI2LTExZWQtODYxZC0wMjQyYWMxMjAwMDIiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJjYWZlZGVmMiIsImF1ZCI6InZtcy5hcGkiLCJpc3MiOiJ2bXMuYXV0aC52MSIsInZtc19hY2NvdW50IjoiMDAwMjgyMDEiLCJleHAiOjE2ODkzNzA1MTIsImlhdCI6MTY4ODc2NTcxMiwianRpIjoiYTM1Nzk3MmRmNzdiNTAwYTQ3NjBlNGY1MGYwNjNhMjUiLCJjbGllbnRfaWQiOiIzMmRhYzMzMDY0OWI0ODI3OWY5Y2FjYzJiNmY1N2FlNSIsInZtc19jbHVzdGVyIjoiYzAxMiJ9.EEmCaFqduOV5_cQ-VejeBXGbKLj1yHrxGkcMgqPUN1jYY0wR9bO7rkEwQh59Dj-1fr8pKsbrUr6DPDLfkaSIRlxpjdlsEBdzAmoZUpzPUfL9QzJu0C04OJU0gcBh7fwur7L2fMVthaZ6OKfThdM29qzRH5RR9gSAJGeNe08n4IvFVvuL80yvczgOiQwSzkg4PuHpbImDa44U7-qC8CrRvQ_TqsX6ziNzw-XmxZxXEwtZxkAOFDFaIsA8V4ezbQ_TSY6EElCnyyKLLI46-KIsGevx8dxa2NrhPFvC725dMhg-OPaCEF62sOlTrlHVBcv_e9MBk1VZLoDLStkSvU7Dxg'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"een.access_token"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "a22ff6c2",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Get Cameras"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"id": "bb457850",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"ret = een.get_list_of_cameras()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"id": "c43f1db1",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"[✅ [1001423e] - ATM & Wine,\n",
|
||||||
|
" ✅ [100d8666] - Cash Register,\n",
|
||||||
|
" ✅ [10012735] - Fuel Dock,\n",
|
||||||
|
" ✅ [1002584c] - Safe]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 7,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"[i for i in een.cameras if i.is_online()]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"id": "f4c6fe67",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"[? [10090759] - Benny Camera,\n",
|
||||||
|
" ? [1003e10b] - Driveway,\n",
|
||||||
|
" ? [100ba388] - Front Door,\n",
|
||||||
|
" ? [100b7b3b] - Max Camera,\n",
|
||||||
|
" ? [1009ae55] - Office]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 8,
|
||||||
|
"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": 9,
|
||||||
|
"id": "74e78ee1-33b8-4a88-9d23-cd6281603a5b",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"2023-07-07T16:41:31.990-05:00 2023-07-07T10:41:31.990-05:00\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"print(een.time_now(), een.time_before())"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 11,
|
||||||
|
"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": 12,
|
||||||
|
"id": "13809cc7-9ec2-4e15-9495-e64feaecca6d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"18"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 12,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"len(een.cameras[2].events['status'])"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 13,
|
||||||
|
"id": "67d4f79b-2b43-4bdb-9068-28ac9d8d921c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 13,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"len(een.cameras[0].events['motion'])"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 14,
|
||||||
|
"id": "8be8d503-b46d-4ba7-884e-2c21c3987129",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"ATM & Wine - 2023-07-07T18:49:29.664+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T18:48:51.444+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-07T16:22:04.146+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T16:21:41.960+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T16:18:16.383+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-07T16:16:31.511+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T16:15:57.174+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-07T15:44:21.759+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T15:41:31.997+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T12:46:47.672+00:00 - bridgeOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T11:24:46.454+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T11:22:37.461+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T11:21:59.127+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-07T09:44:24.462+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T09:41:33.395+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T06:00:38.322+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T06:00:00.175+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-07T04:07:20.003+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T04:05:21.238+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T04:04:42.160+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-07T03:44:26.904+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T03:41:56.630+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-07T01:48:05.792+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-07T01:47:35.155+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T23:19:05.335+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T23:16:58.926+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T23:16:28.421+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T23:10:32.790+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T21:44:28.864+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T21:41:57.860+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T20:08:47.208+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T20:08:16.933+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T20:03:05.801+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T20:02:43.659+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T20:01:41.457+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T19:59:57.405+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T19:59:24.184+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T19:31:55.867+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T19:29:46.708+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T19:29:16.013+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T18:24:27.389+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T18:22:22.328+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T18:21:52.102+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T15:52:12.759+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T15:50:09.509+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T15:49:39.152+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T15:41:58.888+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T15:11:05.317+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T15:10:35.056+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T14:59:51.787+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T14:58:55.970+00:00 - bridgeOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T14:58:40.967+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T12:58:36.884+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T12:56:38.478+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T12:56:07.792+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T12:49:42.742+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T12:47:32.312+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T12:47:01.925+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T11:40:03.505+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T11:38:03.703+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T11:37:23.038+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T11:31:52.611+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T11:29:42.541+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T11:29:08.199+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T09:42:01.013+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T09:38:38.302+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T09:38:07.937+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T08:31:02.432+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T08:28:53.086+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T08:28:22.667+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T04:07:10.931+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T04:05:11.397+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T04:04:38.124+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-06T03:43:19.799+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-06T02:15:51.335+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-06T02:15:19.584+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T21:43:39.111+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T18:35:23.024+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T18:34:44.881+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T17:13:14.356+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T17:11:09.341+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T17:10:29.222+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T15:43:44.544+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T15:33:04.629+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T15:32:34.148+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T15:13:36.151+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T15:11:33.954+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T15:10:54.632+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T14:24:50.082+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T14:22:50.393+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T14:22:12.040+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T14:02:42.746+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T14:00:34.085+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T13:59:55.709+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T10:43:54.157+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T10:41:56.025+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T10:41:23.556+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T10:10:27.404+00:00 - online \n",
|
||||||
|
"ATM & Wine - 2023-07-05T10:08:19.943+00:00 - deviceOffline \n",
|
||||||
|
"ATM & Wine - 2023-07-05T10:07:50.455+00:00 - error \n",
|
||||||
|
"ATM & Wine - 2023-07-05T09:44:11.782+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-07T20:02:49.237+00:00 - bridgeOffline \n",
|
||||||
|
"Cash Register - 2023-07-07T20:02:31.354+00:00 - deviceOffline \n",
|
||||||
|
"Cash Register - 2023-07-07T16:01:54.202+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-07T16:01:52.891+00:00 - bridgeOffline \n",
|
||||||
|
"Cash Register - 2023-07-07T15:44:21.759+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-07T15:41:31.997+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-06T23:01:59.550+00:00 - bridgeOffline \n",
|
||||||
|
"Cash Register - 2023-07-06T21:44:28.864+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-06T21:41:57.860+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-06T14:58:55.385+00:00 - bridgeOffline \n",
|
||||||
|
"Cash Register - 2023-07-06T14:58:40.382+00:00 - deviceOffline \n",
|
||||||
|
"Cash Register - 2023-07-06T10:01:40.245+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-06T10:01:39.158+00:00 - bridgeOffline \n",
|
||||||
|
"Cash Register - 2023-07-06T09:42:01.013+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-05T20:06:43.349+00:00 - bridgeOffline \n",
|
||||||
|
"Cash Register - 2023-07-05T18:01:54.584+00:00 - online \n",
|
||||||
|
"Cash Register - 2023-07-05T18:01:53.582+00:00 - bridgeOffline \n",
|
||||||
|
"Cash Register - 2023-07-05T15:43:44.544+00:00 - online \n",
|
||||||
|
"Safe - 2023-07-06T14:58:55.537+00:00 - bridgeOffline \n",
|
||||||
|
"Safe - 2023-07-06T14:58:40.535+00:00 - deviceOffline \n",
|
||||||
|
"Safe - 2023-07-06T09:42:01.013+00:00 - online \n",
|
||||||
|
"Safe - 2023-07-06T07:27:05.695+00:00 - bridgeOffline \n",
|
||||||
|
"Safe - 2023-07-06T03:43:19.799+00:00 - online \n",
|
||||||
|
"Safe - 2023-07-06T02:05:12.252+00:00 - bridgeOffline \n",
|
||||||
|
"Safe - 2023-07-05T21:43:39.111+00:00 - online \n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"for cam in een.cameras:\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
|
||||||
|
}
|
39
README.md
39
README.md
|
@ -1,40 +1,15 @@
|
||||||
# EE-status-v3 #
|
# EagleEyev3 #
|
||||||
|
|
||||||
## Summary ##
|
## Summary ##
|
||||||
This is a python webapp for getting status history for your cameras.
|
This is a python package for working with the Eagle Eye Networks v3 API. It is all about syntatical sugar and making things a little nicer to work with.
|
||||||
|
|
||||||
## Settings File ##
|
## Settings File ##
|
||||||
There is file `settings.py` that is needed to run. It should look similar to this:
|
There is file `settings.py` that is needed to run. Please reach out to api_support@een.com to get the necessary credentials.
|
||||||
|
|
||||||
```
|
|
||||||
config = {
|
|
||||||
|
|
||||||
# Set up your application and get client id/secrete first
|
|
||||||
# https://developerv3.eagleeyenetworks.com/page/my-application
|
|
||||||
"client_id": "",
|
|
||||||
"client_secret": "",
|
|
||||||
|
|
||||||
# you will need to add approved redirect_uris in your application
|
|
||||||
# this examples assumes you've added http://127.0.0.1:3333/login_callback
|
|
||||||
# change the following variables if you did something different
|
|
||||||
# Note: do not use localhost for server_host, use 127.0.0.1 instead
|
|
||||||
"server_protocol": "http",
|
|
||||||
"server_host": "127.0.0.1",
|
|
||||||
"server_port": "3333",
|
|
||||||
"server_path": "login_callback",
|
|
||||||
|
|
||||||
# preferences
|
|
||||||
"days_of_history": 1,
|
|
||||||
"log_level": "INFO"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can create your application and setup credentials at: [https://developerv3.eagleeyenetworks.com/page/my-application-html](my applications). You can also reach out to api_support@een.com for help.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Ideas on how to use ##
|
## Ideas on how to use ##
|
||||||
|
|
||||||
I encluded an example Flask server in `app.py`. You can run it locally with `python flask run -p 3333 --debug`
|
I encluded an example Flask server in `server.py`. This shows how to wire it up to a login route and handle the callback.
|
||||||
|
|
||||||
It uses server sessions to track your instance of the EagleEyev3 class but doesn't have any other storage. It uses the API as the source of truth.
|
You can also use this in a script by running in iPython in interactive mode `ipython -i server.py`. After you complete to login process, you can hit `CTRL-C` to stop the Flask server. You can then look at the `een` object to get all the instance variables.
|
||||||
|
|
||||||
|
There is also a `Playground.ipynb` that you can run in Jupyter Notebook.
|
||||||
|
|
319
app.py
319
app.py
|
@ -3,164 +3,62 @@ import json, requests
|
||||||
from flask import Flask, request, session, render_template, redirect, url_for, Response, send_file
|
from flask import Flask, request, session, render_template, redirect, url_for, Response, send_file
|
||||||
from flask_session import Session
|
from flask_session import Session
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
from matplotlib.figure import Figure
|
|
||||||
import matplotlib.dates as mdates
|
|
||||||
import base64
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from io import BytesIO
|
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 EagleEyev3
|
||||||
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 = 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()
|
||||||
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")
|
||||||
|
|
||||||
|
|
||||||
|
#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,103 +66,55 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/cameras')
|
@app.route('/cameras')
|
||||||
@login_required
|
|
||||||
def cameras():
|
def cameras():
|
||||||
|
if 'een' in session:
|
||||||
|
een = session['een']
|
||||||
|
else:
|
||||||
|
een = EagleEyev3()
|
||||||
|
|
||||||
# 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')
|
@app.route('/camera/<esn>/live_preview')
|
||||||
@login_required
|
def camera_live_preivew(esn=None):
|
||||||
def accounts():
|
if 'een' in session:
|
||||||
|
een = session['een']
|
||||||
# get een instance from session, @login_required already checked for it
|
else:
|
||||||
een = session['een']
|
een = EagleEyev3()
|
||||||
|
|
||||||
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']
|
|
||||||
|
|
||||||
camera = een.get_camera_by_id(esn)
|
camera = een.get_camera_by_id(esn)
|
||||||
res = camera.get_live_preview()
|
res = camera.get_live_preview()
|
||||||
|
@ -275,14 +125,16 @@ def camera__preivew_image(esn=None):
|
||||||
return send_file('static/placeholder.png')
|
return send_file('static/placeholder.png')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/camera/<esn>/preview')
|
@app.route('/camera/<esn>')
|
||||||
@login_required
|
def camera_detail(esn=None):
|
||||||
def camera_live_preivew(esn=None):
|
if 'een' in session:
|
||||||
|
een = session['een']
|
||||||
# get een instance from session, @login_required already checked for it
|
else:
|
||||||
een = session['een']
|
een = EagleEyev3()
|
||||||
|
|
||||||
camera = een.get_camera_by_id(esn)
|
camera = een.get_camera_by_id(esn)
|
||||||
|
camera.get_list_of_events(end_timestamp=een.time_before(ts=een.time_now(), hours=0), \
|
||||||
|
start_timestamp=een.time_before(ts=een.time_now(), hours=6) )
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
"current_user": een.current_user,
|
"current_user": een.current_user,
|
||||||
|
@ -290,84 +142,7 @@ def camera_live_preivew(esn=None):
|
||||||
"events": camera.events['status']
|
"events": camera.events['status']
|
||||||
}
|
}
|
||||||
|
|
||||||
return render_template('camera_preview.html', template_values=values)
|
return render_template('camera_detail_partial.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
|
|
||||||
een = session['een']
|
|
||||||
|
|
||||||
camera = een.get_camera_by_id(esn)
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
"camera": camera,
|
|
||||||
"events": camera.events['status']
|
|
||||||
}
|
|
||||||
|
|
||||||
return render_template('camera_events_partial.html', template_values=values)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/camera/<esn>/status_plot')
|
|
||||||
@login_required
|
|
||||||
def camera_status_plot(esn=None):
|
|
||||||
|
|
||||||
# get een instance from session, @login_required already checked for it
|
|
||||||
een = session['een']
|
|
||||||
|
|
||||||
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', '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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Generate the figure **without using pyplot**.
|
|
||||||
fig = Figure(figsize=(6, 5), dpi=160)
|
|
||||||
ax = fig.subplots()
|
|
||||||
|
|
||||||
ax.step(data.index, data['status'], lw=2, color='blue')
|
|
||||||
ax.set_title(cam.name)
|
|
||||||
# ax.fill_between(data['startTimestamp'], data['status'])
|
|
||||||
ax.axhline(1, color='green', lw=2, alpha=0.3)
|
|
||||||
ax.axhline(0, color='red', lw=2, alpha=0.3)
|
|
||||||
|
|
||||||
for label in ax.get_xticklabels(which='major'):
|
|
||||||
label.set(rotation=30, horizontalalignment='right')
|
|
||||||
|
|
||||||
|
|
||||||
# Save it to a temporary buffer.
|
|
||||||
buf = BytesIO()
|
|
||||||
fig.savefig(buf, format="png", transparent=True)
|
|
||||||
# Embed the result in the html output.
|
|
||||||
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>"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -376,4 +151,4 @@ def camera_status_plot(esn=None):
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
build: .
|
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
ports:
|
|
||||||
- "9300:3000"
|
|
||||||
restart: always
|
|
||||||
tty: true
|
|
||||||
user: 1000:1000
|
|
||||||
|
|
|
@ -9,13 +9,7 @@ idna==3.4
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
MarkupSafe==2.1.3
|
MarkupSafe==2.1.3
|
||||||
|
pytz==2023.3
|
||||||
|
requests==2.29.0
|
||||||
urllib3==1.26.16
|
urllib3==1.26.16
|
||||||
Werkzeug==2.3.6
|
Werkzeug==2.3.6
|
||||||
gunicorn==20.1.0
|
|
||||||
cachelib==0.10.2
|
|
||||||
Flask-Session==0.5.0
|
|
||||||
EagleEyev3>=0.0.18
|
|
||||||
tqdm
|
|
||||||
pandas
|
|
||||||
numpy
|
|
||||||
matplotlib
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
#python3 -u server.py
|
|
||||||
gunicorn --bind 0.0.0.0:3000 --limit-request-line 0 --worker-class=gthread --threads=4 wsgi:app
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
106
static/cover.css
106
static/cover.css
|
@ -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.
5528
static/theme.css
5528
static/theme.css
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,24 @@
|
||||||
<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;">
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>{{ template_values['camera'].name }}</h3>
|
||||||
|
<img src="/camera/{{ template_values['camera'].id }}/live_preview" style="width: 100%; max-height:360px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6" id="events_list">
|
||||||
|
<h3>Events</h3>
|
||||||
|
<ul>
|
||||||
|
{% if template_values['events'] %}
|
||||||
|
{% for event in template_values['events'] %}
|
||||||
|
<li>{{ event['data'][0]['newStatus']['connectionStatus'] }} <br> <small>{{ event['startTimestamp'] }}</small></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<li>No events in the last six hours</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<!-- <button hx-get="/camera/{{ template_values['camera'].id }}/events" hx-trigger="click" hx-target="#event_list">refresh</button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
<h3>List of Events <i class="bi bi-calendar-event"></i></h3>
|
|
||||||
<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">
|
|
||||||
<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'] %}
|
|
||||||
<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 %}
|
|
|
@ -1,3 +0,0 @@
|
||||||
<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; max-width:100%">
|
|
|
@ -1,61 +1,28 @@
|
||||||
|
|
||||||
<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() %}
|
|
||||||
<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 %}
|
|
||||||
|
|
||||||
<h5>Offline <small>[{{ template_values['camera_count_offline'] }} of {{ template_values['camera_count'] }}]</small></h5>
|
|
||||||
{% for camera in template_values['cameras'] %}
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
|
|
||||||
<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>
|
<div class="col-md-3" id="camera_list_items">
|
||||||
<br>
|
<h3>Online</h3>
|
||||||
|
<ul>
|
||||||
|
{% for camera in template_values['cameras'] %}
|
||||||
|
{% if camera.is_online() %}
|
||||||
|
<li hx-get="/camera/{{ camera.id }}" hx-trigger="click" hx-target="#camera_detail"> {{ camera.name }}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Offline</h3>
|
||||||
|
<ul>
|
||||||
|
{% for camera in template_values['cameras'] %}
|
||||||
|
{% if not camera.is_online() %}
|
||||||
|
<li hx-get="/camera/{{ camera.id }}" hx-trigger="click" hx-target="#camera_detail"> {{ camera.name }}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<div class="col-md-6 offset-3">
|
||||||
|
<button hx-get="/cameras" hx-trigger="click" hx-target="#camera_list">refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-9" id="camera_detail"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -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,32 +22,10 @@
|
||||||
|
|
||||||
{% 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" id="camera_list">
|
||||||
<div class="col-md-5" id="camera_list" style="max-height:400px; overflow:auto;">
|
{% include 'cameras_partial.html' %}
|
||||||
{% include 'cameras_partial.html' %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7" id="camera_detail">
|
|
||||||
<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 <i class="bi bi-calendar-event"></i></h2>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7" id="camera_status_plot">
|
|
||||||
<h2>Graph of Status Events <i class="bi bi-bar-chart"></i></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue