Compare commits
59 Commits
full_examp
...
main
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 | |
Mark Cotton | 46d9423890 | |
Mark Cotton | ee29551175 | |
Mark Cotton | 5466aeee74 | |
Mark Cotton | f4676c2257 | |
Mark Cotton | 8eb3a45e2e | |
Mark Cotton | 9ad71170c8 | |
Mark Cotton | fbf09eb767 | |
Mark Cotton | aea2873ac4 | |
Mark Cotton | 36336c72c1 | |
Mark Cotton | 2441e86dbc | |
Mark Cotton | df38fd7f74 | |
Mark Cotton | a26ef602fa | |
mcotton | 842d067502 |
|
@ -8,3 +8,5 @@ my_settings.py
|
|||
.ipynb_checkpoints/
|
||||
.ipynb_checkpoints/*
|
||||
flask_session/
|
||||
git-version.txt
|
||||
settings.py
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
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" ]
|
|
@ -1,641 +0,0 @@
|
|||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
# 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"
|
|
@ -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.
|
459
Playground.ipynb
459
Playground.ipynb
|
@ -1,459 +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": [],
|
||||
"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,15 +1,40 @@
|
|||
# EagleEyev3 #
|
||||
# EE-status-v3 #
|
||||
|
||||
## Summary ##
|
||||
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.
|
||||
This is a python webapp for getting status history for your cameras.
|
||||
|
||||
## Settings File ##
|
||||
There is file `settings.py` that is needed to run. Please reach out to api_support@een.com to get the necessary credentials.
|
||||
There is file `settings.py` that is needed to run. It should look similar to this:
|
||||
|
||||
```
|
||||
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 ##
|
||||
|
||||
I encluded an example Flask server in `server.py`. This shows how to wire it up to a login route and handle the callback.
|
||||
I encluded an example Flask server in `app.py`. You can run it locally with `python flask run -p 3333 --debug`
|
||||
|
||||
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.
|
||||
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.
|
||||
|
|
319
app.py
319
app.py
|
@ -3,62 +3,164 @@ import json, requests
|
|||
from flask import Flask, request, session, render_template, redirect, url_for, Response, send_file
|
||||
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
|
||||
|
||||
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 EagleEyev3
|
||||
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()
|
||||
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")
|
||||
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,
|
||||
"accounts": een.accounts,
|
||||
"active_account": een.active_account
|
||||
}
|
||||
redirect(url_for('accounts'))
|
||||
|
||||
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()]),
|
||||
"accounts": een.accounts,
|
||||
"active_account": een.active_account
|
||||
}
|
||||
|
||||
return render_template('index.html', template_values=values)
|
||||
|
@ -66,55 +168,103 @@ 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:
|
||||
een = EagleEyev3()
|
||||
# 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')
|
||||
|
||||
return redirect(url_for('index'))
|
||||
|
||||
|
||||
@app.route('/cameras')
|
||||
@login_required
|
||||
def cameras():
|
||||
if 'een' in session:
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3()
|
||||
|
||||
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>/live_preview')
|
||||
def camera_live_preivew(esn=None):
|
||||
if 'een' in session:
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3()
|
||||
@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']
|
||||
|
||||
camera = een.get_camera_by_id(esn)
|
||||
res = camera.get_live_preview()
|
||||
|
@ -125,16 +275,14 @@ def camera_live_preivew(esn=None):
|
|||
return send_file('static/placeholder.png')
|
||||
|
||||
|
||||
@app.route('/camera/<esn>')
|
||||
def camera_detail(esn=None):
|
||||
if 'een' in session:
|
||||
een = session['een']
|
||||
else:
|
||||
een = EagleEyev3()
|
||||
@app.route('/camera/<esn>/preview')
|
||||
@login_required
|
||||
def camera_live_preivew(esn=None):
|
||||
|
||||
# get een instance from session, @login_required already checked for it
|
||||
een = session['een']
|
||||
|
||||
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 = {
|
||||
"current_user": een.current_user,
|
||||
|
@ -142,7 +290,84 @@ def camera_detail(esn=None):
|
|||
"events": camera.events['status']
|
||||
}
|
||||
|
||||
return render_template('camera_detail_partial.html', template_values=values)
|
||||
return render_template('camera_preview.html', template_values=values)
|
||||
|
||||
@app.route("/camera/<esn>/events/<int:days>")
|
||||
@app.route('/camera/<esn>/events')
|
||||
@login_required
|
||||
def camera_detail(esn=None, days=DAYS_OF_HISTORY):
|
||||
|
||||
# get een instance from session, @login_required already checked for it
|
||||
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>"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -151,4 +376,4 @@ def camera_detail(esn=None):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=een.server_host, port=een.server_port)
|
||||
app.run(host=config.server_host, port=config.server_port)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
volumes:
|
||||
- .:/app
|
||||
ports:
|
||||
- "9300:3000"
|
||||
restart: always
|
||||
tty: true
|
||||
user: 1000:1000
|
||||
|
|
@ -9,7 +9,13 @@ idna==3.4
|
|||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
pytz==2023.3
|
||||
requests==2.29.0
|
||||
urllib3==1.26.16
|
||||
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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
#!/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
|
@ -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,24 +1,3 @@
|
|||
|
||||
|
||||
<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>
|
||||
<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;">
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<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 %}
|
|
@ -0,0 +1,3 @@
|
|||
<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,28 +1,61 @@
|
|||
|
||||
<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 %}
|
||||
|
||||
|
||||
<div class="col-md-3" id="camera_list_items">
|
||||
<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>
|
||||
|
||||
|
||||
<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,10 +126,32 @@
|
|||
|
||||
{% 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" id="camera_list">
|
||||
{% include 'cameras_partial.html' %}
|
||||
<div class="row">
|
||||
<div class="col-md-5" id="camera_list" style="max-height:400px; overflow:auto;">
|
||||
{% 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>
|
||||
|
|
Loading…
Reference in New Issue