Compare commits

...

4 Commits

8 changed files with 240 additions and 61 deletions

View File

@ -6,6 +6,7 @@ from .settings import *
from datetime import datetime, timedelta
from pytz import timezone
from io import BytesIO
logging.basicConfig(level=logging.INFO)
@ -466,6 +467,19 @@ class EagleEyev3():
}
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
@ -484,13 +498,21 @@ class Device():
return self.id
def get_status(self):
return self.status['connectionStatus']
if 'connectionStatus' in self.status:
return self.status['connectionStatus']
return None
def is_online(self):
return self.status['connectionStatus'] == "online"
if 'connectionStatus' in self.status:
return self.status['connectionStatus'] == "online"
return None
def is_offline(self):
return self.status['connectionStatus'] == "deviceOffline" or self.status['connectionStatus'] == "bridgeOffline"
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():
@ -525,6 +547,7 @@ class Camera(Device):
"""
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,
@ -538,32 +561,81 @@ class Camera(Device):
"Accept": "application/json"
}
response = requests.get(url, headers=headers)
response_json = response.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")
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']
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'])]
# 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
# 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

75
app.py
View File

@ -1,8 +1,10 @@
import json, requests
from flask import Flask, request, session, render_template, redirect, url_for
from flask import Flask, request, session, render_template, redirect, url_for, Response, send_file
from flask_session import Session
from io import BytesIO
import logging
logger = logging.getLogger()
@ -46,8 +48,17 @@ def index():
logging.info(f"{check['success']} - check get_current_user")
#logging.info(een.cameras)
# this call could get expensive
een.get_list_of_cameras()
#logging.info(een.cameras)
values = {
"current_user": een.current_user
"current_user": een.current_user,
"cameras": een.cameras
}
return render_template('index.html', template_values=values)
@ -68,7 +79,7 @@ def login_callback():
# use the include code parameter to complete login process
oauth_object = een.login_tokens(code)
return redirect(url_for('index'))
@ -81,5 +92,63 @@ def logout():
return redirect(url_for('index'))
@app.route('/cameras')
def cameras():
if 'een' in session:
een = session['een']
else:
een = EagleEyev3()
een.get_list_of_cameras()
values = {
"current_user": een.current_user,
"cameras": een.cameras
}
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()
camera = een.get_camera_by_id(esn)
res = camera.get_live_preview()
if res:
return send_file(BytesIO(res.content), mimetype='image/jpeg')
else:
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()
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,
"camera": camera,
"events": camera.events['status']
}
return render_template('camera_detail_partial.html', template_values=values)
if __name__ == '__main__':
app.run(host=een.server_host, port=een.server_port)

BIN
static/placeholder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
static/placeholder1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -1,5 +1,4 @@
{% if "X-PJAX" not in request.headers %}
<!doctype html>
<html lang="en">
<head>
@ -10,7 +9,7 @@
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
{% block title %}
<title>classifier.een.cloud</title>
<title>status.mcotton.space</title>
{% endblock %}
<!-- Bootstrap core CSS -->
@ -61,14 +60,11 @@
<main role="main">
{% endif %}
{% block main %}
{% endblock %}
{% if "X-PJAX" not in request.headers %}
</main>
<footer class="text-muted">
@ -88,35 +84,14 @@
<script src="//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js"></script>
<script src="/static/js/jquery.pjax.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
<script>
$(document).ready(function() {
$('#user_mode_toggle').on('click', function(event) {
event.stopPropagation();
$.ajax({
type: "POST",
url: '/api/v1/user_mode/toggle',
dataType: "text",
contentType : 'application/json',
success: function(data) {
//$('#labelModal-add').text('Saved');
//$('#labelModal').modal('hide');
location.reload();
},
error: function(data) {
console.log(data)
$('#labelModal-add').text('Failed');
//$('#labelModal').modal('hide');
//location.reload();
}
});
});
});
@ -128,4 +103,3 @@
{% endblock%}
</body>
</html>
{% endif %}

View File

@ -0,0 +1,24 @@
<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>

View File

@ -0,0 +1,28 @@
<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>

View File

@ -1,6 +1,18 @@
{% extends "base.html" %}
{% block style %}
<style>
#camera_list_items {
max-height: 500px;
overflow: auto;
}
#events_list {
max-height: 500px;
overflow: auto;
}
</style>
{% endblock %}
{% block current_user %}
@ -10,12 +22,12 @@
{% block main %}
<div class="container text-center">
<div class="row">
<div class="col-md-6 offset-3">
<h1>Hello {{ template_values['current_user']['firstName'] }} {{ template_values['current_user']['lastName'] }}</h1>
</div>
</div>
<div class="container text-center">
<div class="row" id="camera_list">
{% include 'cameras_partial.html' %}
</div>
</div>
{% endblock %}