First major contributions in an interim state #1

Merged
lade043 merged 8 commits from First-major-contributions into master 2021-05-30 10:07:15 +02:00
19 changed files with 718 additions and 0 deletions
+9
View File
@@ -127,3 +127,12 @@ dmypy.json
# Pyre type checker
.pyre/
# Log files of RasperryPie
*.log
# File containing secrets (credentials) and config
*.cfg
AwningV2.py
+11
View File
@@ -0,0 +1,11 @@
{
"cSpell.enabled": false,
"sqltools.connections": [
{
"previewLimit": 50,
"driver": "SQLite",
"name": "database",
"database": "${workspaceFolder:RaspberryPie}/RaspberryPie/database.db"
}
]
}
+7
View File
@@ -1,2 +1,9 @@
# RaspberryPie
A python application for Raspberry Pi controlling GPIO via gmail and many more features.
## Features
With this application you can control GPIO pins via Email, view a live image taken via the Raspi-Cam and temperature measurements in a Dropbox.
But wherefore do you need it? For examle if you have an awning, which shouldn't get wet thus you want to control from far away... And many more, this is just what I use it for.
## Installation
This section will be following.
+4
View File
@@ -0,0 +1,4 @@
import configparser
config = configparser.RawConfigParser()
config.read("config.cfg") # edit this file for your specific configuration
+104
View File
@@ -0,0 +1,104 @@
#general imports
import datetime
import sqlite3
import base64
# imports from Project
import RaspberryPie.config as config
captScribe = None
def _set_captScribe(_captScribe):
global captScribe
captScribe = _captScribe
class BGenSecretary:
def __init__(self, database_path, image_path):
self.database = database_path
self.image_path = image_path
def _record(self, data: dict):
connection = None
try:
connection = sqlite3.connect(self.database)
cursor = connection.cursor()
for typ in data:
time = datetime.datetime.now()
data[typ]["time_string"] = str(time)
data[typ]["unix_time"] = int(time.timestamp())
command = "INSERT INTO {} ({}) VALUES ({})".format(typ, str([i for i in data[typ]])[1:-1], str([data[typ][i] for i in data[typ]])[1:-1])
cursor.execute(command)
captScribe.info("Inserted data into table '{}' of database".format(typ), "BGenSecretary._record")
connection.commit()
with open(config.config["FileLocations"]["lastChangeFile"], 'w') as f:
f.write(str(int(datetime.datetime.now().timestamp())))
except sqlite3.Error as e:
captScribe.error(str(e), "BGenSecretary._record")
finally:
if connection is sqlite3.Connection:
connection.close()
def recordPicture(self, picture: bytes):
with open("{}/{}.jpg".format(self.image_path, str(datetime.datetime.now())), "w") as f:
f.write(picture)
self._record({"picture": {"picture": base64.encodebytes(picture).decode('ascii').replace("\n", "")}})
def recordAir(self, data):
self._record({"air": {"temperature": data[0], "humidity": data[1]}})
def _fetchSingle(self, table, time: datetime.datetime):
unix_time = int(time.timestamp())
command = "SELECT * FROM {} ORDER BY ABS(unix_time - {}) ASC LIMIT 1".format(table, unix_time)
connection, entry = None
try:
connection = sqlite3.connect(self.database)
cursor = connection.cursor()
cursor.execute(command)
entry = cursor.fetchone()
captScribe.info("Fetched entry for {} from {}".format(str(unix_time), table), "BGenSecretary._fetchSingle")
except sqlite3.Error as e:
captScribe.error(str(e), "BGenSecretary._fetchSingle")
finally:
if connection is sqlite3.Connection:
connection.close()
return tuple(entry) if entry is sqlite3.Row else None
def _fetchMultiple(self, table, end:datetime.datetime, start: datetime.datetime):
unix_time_end = int(end.timestamp())
unix_time_start = int(start.timestamp())
command = "SELECT * FROM {} WHERE unix_time BETWEEN {} AND {}".format(unix_time_start, unix_time_end)
connection, entries = None
try:
connection = sqlite3.connect(self.database)
cursor = connection.cursor()
cursor.execute(command)
entries = cursor.fetchall()
captScribe.info("Fetched entries for {} from {}".format(str((unix_time_start, unix_time_end)), table), "BGenSecretary._fetchSingle")
except sqlite3.Error as e:
captScribe.error(str(e), "BGenSecretary._fetchSingle")
finally:
if connection is sqlite3.Connection:
connection.close()
return [tuple(entry) for entry in entries] if entries is not None else None
def fetchAir(self, time=datetime.datetime.now(), start_time=None): # time is either one point or the end point if start is given
if not start_time:
entry = self._fetchSingle("air", time)
return {"unix_time": entry[0], "time_string": entry[1], "temperature": entry[2], "humidity": entry[3]}
else:
entries = self._fetchMultiple("air", time, start_time)
return {"unix_time": [entry[0] for entry in entries], "time_string": [entry[1] for entry in entries], "temperature": [entry[2] for entry in entries], "humidity": [entry[3] for entry in entries]}
def fetchPicture(self, time=datetime.datetime.now(), start_time=None):
if not start_time:
entry = self._fetchSingle("picture", time)
return {"unix_time": entry[0], "time_string": entry[1], "picture": entry[2]}
else:
entries = self._fetchMultiple("picture", time, start_time)
return {"unix_time": [entry[0] for entry in entries], "time_string": [entry[1] for entry in entries], "picture": [entry[2] for entry in entries]}
bGenSecretary = BGenSecretary(config.config["File Locations"]["database_file"], config.config["File Locations"],["image_path"])
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
import datetime
import RaspberryPie.config as config
import RaspberryPie.dataHandling as dataHandling
def time(): return ""
def weatherUrls(): return [config.config["WeatherUrls"][i] for i in config.config["WeatherUrls"]]
def getLatestAirData(): return dataHandling.bGenSecretary.fetchAir()
def getLatestPicture(): return dataHandling.bGenSecretary.fetchAir()
def getDataRangeAir(start: int, end: int): return dataHandling.bGenSecretary.fetchAir(datetime.datetime.utcfromtimestamp(end), datetime.datetime.utcfromtimestamp(start))
def getDataRangePictures(start: int, end: int): return dataHandling.bGenSecretary.fetchPicture(datetime.datetime.utcfromtimestamp(end), datetime.datetime.utcfromtimestamp(start))
def getSingleAirData(time: int): return dataHandling.bGenSecretary.fetchAir(datetime.datetime.utcfromtimestamp(time))
def getSinglePicture(time: int): return dataHandling.bGenSecretary.fetchPicture(datetime.datetime.utcfromtimestamp(time))
def lastChange():
with open(config.config["FileLocations"]["lastChangeFile"], 'r') as f:
return int(f.read())
+61
View File
@@ -0,0 +1,61 @@
from flask import Flask, render_template, make_response
import json
import signal
import RaspberryPie.flaskAddons as fa
app = Flask(__name__)
@app.route("/")
def index():
latestAirData = fa.getLatestAirData()
latestPicture = fa.getLatestPicture()
return render_template("index.html",
WeatherTime = latestAirData["time_string"],
temp = latestAirData["temperature"],
humi = latestAirData["humidity"],
weather_urls=fa.weatherUrls(),
PictureTime = latestPicture["time_string"],
picture = latestPicture["picture"])
@app.route("/data/<start>/<end>/")
def airData(start, end):
data = fa.getDataRangeAir(int(start), int(end))
return json.dumps(data)
@app.route("/picture/<time>/")
def picture(time):
if time == "latest":
return json.dumps(fa.getLatestPicture())
else:
return json.dumps(fa.getLatestPicture(int(time)))
@app.route("/initiate/picture/")
def inititatePicture():
pass
@app.route("/inititate/air/")
def ininitiateAir():
pass
@app.route("/initiate/gpio/<state>/")
def ininitisteGPIO(state):
try:
state = int(state)
except:
state = state
if state == "open" or state == 0:
return "Opened", 200
elif state == "close" or state == 1:
return "Closed", 200
return "state not found", 404
@app.route("/initiate/shutdown/")
def initiateShutdown():
pass
@app.route("/lastChange/")
def lastChange():
return str(fa.lastChange())
+9
View File
@@ -0,0 +1,9 @@
#! /usr/bin/python3
import logging
import sys
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0, '/home/pi/RaspberryPie/RaspberryPie/')
from flask_backend import app as application
application.secret_key = "Whatever"
+130
View File
@@ -0,0 +1,130 @@
#general imports
from RaspberryPie.logger import CaptScribe
import time
import datetime
# imports for hardware
import Adafruit_DHT
from picamera import PiCamera
import RPi.GPIO as GPIO
# imports from project
import RaspberryPie.config as config
import RaspberryPie.dataHandling as dataHandling
captScribe = None
def _set_captScribe(_captScribe):
global captScribe
captScribe = _captScribe
dataHandling._set_captScribe(_captScribe)
class MajGenGPIOController:
def __init__(self, pin_open, pin_close, delta_switchOff):
self.pinOpen = pin_open
self.pinClose = pin_close
self.deltaOff = delta_switchOff
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.pinOpen, GPIO.OUT)
GPIO.setup(self.pinClose, GPIO.OUT)
GPIO.output(self.pinOpen, GPIO.HIGH)
GPIO.output(self.pinClose, GPIO.HIGH)
def finalMission(self):
self.kill()
GPIO.cleanup()
def close(self):
self.kill()
GPIO.output(self.pinClose, GPIO.LOW)
GPIO.output(self.pinOpen, GPIO.HIGH)
captScribe.info("GPIO set to closing", "MajGenGPIOControl.close")
def open(self):
self.kill()
GPIO.output(self.pinClose, GPIO.HIGH)
GPIO.output(self.pinOpen, GPIO.LOW)
captScribe.info("GPIO set to opening", "MajGenGPIOControl.open")
def kill(self):
GPIO.output(self.pinClose, GPIO.HIGH)
GPIO.output(self.pinOpen, GPIO.HIGH)
time.sleep(.5) # waiting, so relay definetly has switched
captScribe.info("GPIO switched off", "MajGenGPIOControl.kill")
def delayedSwitchOff(self):
time.sleep(self.deltaOff)
self.kill()
class MajGenObserver:
class DictatingMachine: # just a file like object
def __init__(self):
self.captured = ""
def write(self, text):
self.captured = text
def __str__(self):
return self.captured
def __init__(self, schedule_timing):
self.schedule_timing = schedule_timing
def get_schedule(self):
return self.schedule_timing
class MajGenAirChecker(MajGenObserver):
def __init__(self, schedule_timing=None):
if not schedule_timing:
schedule_timing = config.config["Timing"]["environmentdelta"]
super().__init__(schedule_timing)
def execute():
sensor = Adafruit_DHT.DHT22
pin = int(config.config["Hardware"]["dhtpin"])
humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
if humidity is None or temperature is None:
captScribe.warning("Error while reading DHT22 sensor. The received values are: Humidity: {}, Temperature: {}".format(str(humidity), str(temperature)), "MajGenObserver.get_sensorData")
humidity, temperature = (-1, -274) # if you need any numbers, why don't take impossible ones, to show error
return humidity, temperature
class MajGenVisualObserver(MajGenObserver):
def __init__(self, schedule_timing=None):
if not schedule_timing:
schedule_timing = config.config["Timing"]["picturedelta"]
super().__init__(schedule_timing)
def execute():
camera = PiCamera()
majGensDictatingMachine = super.DictatingMachine()
camera.start_preview()
time.sleep(5) # letting camera adjust to environment (exposure and wb)
camera.capture(majGensDictatingMachine, format="jpeg")
camera.stop_preview()
return str(majGensDictatingMachine)
class MajGenAirRecoder(MajGenAirChecker):
def __init__(self, dataCollector, schedule_timing=None):
self.dataCollector = dataCollector
super().__init__(schedule_timing=schedule_timing)
def execute(self):
data = super.execute()
self.dataCollector.recordeAir({"air": (str(datetime.datetime.now()), data)})
class MajGenVisualRecoder(MajGenVisualObserver):
def __init__(self, dataCollector, schedule_timing=None):
self.dataCollector = dataCollector
super().__init__(schedule_timing=schedule_timing)
def execute(self):
data = super.execute()
self.dataCollector.recordePicture({"picture": (str(datetime.datetime.now()), data)})
majGenGPIOController = MajGenGPIOController(config.config["Hardware"]["pinopen"], config.config["Hardware"]["pinclose"], config.config["Timing"]["gpioswitchoff"])
majGenVisualObserver = MajGenVisualObserver()
majGenAirChecker = MajGenAirChecker()
majGenAirRecoder = MajGenAirRecoder(dataHandling.bGenSecretary)
majGenVisualRecoder = MajGenVisualRecoder(dataHandling.bGenSecretary)
+126
View File
@@ -0,0 +1,126 @@
import dropbox
import requests
import socket
import html2text
import imaplib
import email
import datetime
import RaspberryPie.config as config
captScribe = None
def _set_captScribe(_captScribe):
global captScribe
captScribe = _captScribe
class MajGenApiCom:
class Telegram:
def __init__(self, id, subject, sender, time, message):
self.id = id
self.subject = subject
self.sender = sender
self.time = time
self.message = message
def __str__(self):
return "({}){}: {} at {}: {}".format(self.id, self.sender, self.subject, self.time, self.message)
def delete(self):
MajGenApiCom.delete_mail(id)
def __init__(self, email, dropbox):
self.email = email
self.dropbox = dropbox
self.mail_config = config.config["Mail"]
def upload_dropbox(self, filename, content):
try:
dbx = dropbox.Dropbox(self.dropbox)
if type(content) != bytes:
content = content.encode('utf-8')
dbx.files_upload(content, filename, mode=dropbox.files.WriteMode.overwrite)
captScribe.info("Uploaded file: {}".format(filename), "MajGenApiCom.upload_dropbox")
except (dropbox.dropbox.ApiError, dropbox.dropbox.AuthError, dropbox.dropbox.BadInputError,
dropbox.dropbox.HttpError, dropbox.dropbox.InternalServerError, dropbox.dropbox.PathRootError,
dropbox.dropbox.RateLimitError, requests.exceptions.ReadTimeout, requests.exceptions.ConnectTimeout,
requests.exceptions.Timeout, requests.exceptions.ConnectionError) as dropbox_error:
captScribe.error("{} occured, while trying to upload {}.".format(str(dropbox_error), filename), "MajGenApiCom.upload_dropbox")
def get_emails(self):
mail_box = []
imap = None
if not "@" + self.mail_config["url"] in self.email["address"]:
add_url = True
else:
add_url = False
try:
imap = imaplib.IMAP4_SSL(self.mail_config["smtpserver"])
complete_mail_adress = lambda: self.email["address"] + "@" + self.mail_config["url"] if add_url else self.email["address"]
imap.login(complete_mail_adress(), self.email["password"])
imap.select("inbox")
type, data = imap.search(None, 'ALL')
ids = data[0]
for id in reversed(ids.split()):
text = ""
typ, content = imap.fetch(id, '(RFC822)')
for part in content:
if isinstance(part, tuple):
message = email.message_from_string(part[1].decode('utf-8'))
# credit for next 7 lines: https://stackoverflow.com/users/1105597/jury
for part in message.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.get_content_maintype() == 'text':
# reading as HTML (not plain text)
_html = part.get_payload(decode = True)
text = html2text.html2text(_html)
telegram = self.Telegram(id, email.Header.decode_header(message["Subject"])[0][0],
email.utils.parseaddr(message['From']),
datetime.fromtimestamp(email.utils.mktime_tz(email.utils.parsedate_tz(message['Date']))),
text)
mail_box.append(telegram)
captScribe.info("Gotten email: {}".format(str(telegram)), "MajGenApiCom.get_emails")
except (imaplib.IMAP4.error, socket.error) as imap_error:
captScribe.error("Getting emails failed with: {}.".format(str(imap_error)), "MajGenApiCom.get_emails")
finally:
try:
if imap:
imap.close()
imap.logout()
except:
captScribe.error("Logout of imap failed.", "MajGenApiCom.get_emails")
def delete_mail(self, id):
imap = None
if not "@" + self.mail_config["url"] in self.email["address"]:
add_url = True
else:
add_url = False
try:
imap = imaplib.IMAP4_SSL(self.mail_config["smtpserver"])
complete_mail_adress = lambda: self.email["address"] + "@" + self.mail_config["url"] if add_url else self.email["address"]
imap.login(complete_mail_adress(), self.email["password"])
imap.select("inbox")
imap.store(id, '+FLAGS', '\\DELETED')
captScribe.info("Deleted email with id {}".format(id), "MajGenApiCom.delete_mail")
except (imaplib.IMAP4.error, socket.error) as imap_error:
captScribe.error("Getting emails failed with: {}.".format(str(imap_error)), "MajGenApiCom.delete_mail")
finally:
try:
imap.expunge()
imap.close()
imap.logout()
except:
captScribe.error("Logout of imap failed.", "MajGenApiCom.delete_mail")
majGenApiCom = MajGenApiCom({"address": config.config["Secrets"]["emailaddress"], "password": config.config["Secrets"]["emailpassword"]}, config.config["Secrets"]["dropboxtoken"])
+50
View File
@@ -0,0 +1,50 @@
var chrtArea = document.getElementById("EnvironmentChart").getContext('2d');
var chrt = new Chart(chrtArea, {
type: 'line',
data: {},
options: {
scales: {
xAxes: [{
ticks: {
maxTicksLimit: 8
}
}],
yAxes:[{
id: "Temp",
type: "linear",
position: "left"
}, {
id: "Humi",
type: "linear",
position: "right"
}]
}
}
});
function getEnvironmentData(range){
var response = fetch("/data/environment/" + range[0] + "/" + range[1])
var labels = [];
response.labels.forEach(element => {
labels.push(unixToHuman(element))
});
return {
labels: labels,
datasets: [{
label: "Temperature",
yAxisID: "Temp",
data: response.temp
},
{
label: "Humidity",
yAxisID: "Humi",
data: response.humi
}]
};
}
function unixToHuman(unix){
var str = new Date(unix).toLocaleTimeString()
return str
}
+39
View File
@@ -0,0 +1,39 @@
# general imports
import logging
class CaptScribe:
def __init__(self, log_info, log_error):
self.logfile_info = log_info
self.logfile_error = log_error
self.loggers = [logging.getLogger("log_info"), logging.getLogger("log_error")]
self.formatters = [logging.Formatter("%(asctime)s : %(level), %(message)s"), logging.Formatter("%(asctime)s : %(message)s: %(sinfo)")]
self.handlers = [logging.TimedRotatingFileHandler(self.logfile_info, when='midnight', interval=1), logging.TimedRotatingFileHandler(self.logfile_error, when='midnight', interval=1)]
for i, handler in enumerate(self.handlers):
handler.setFormatter(self.formatters[i])
for i, logger in self.loggers:
logger.addHandler(self.handlers[i])
self.loggers[0].level = logging.INFO
self.loggers[1].level = logging.ERROR
def critical(self, msg, func=""):
for logger in self.loggers:
logger.critical(func.upper() + ": " + msg)
def error(self, msg, func=""):
for logger in self.loggers:
logger.error(func.upper() + ": " + msg)
def warning(self, msg, func=""):
for logger in self.loggers:
logger.warning(func.upper() + ": " + msg)
def info(self, msg, func=""):
for logger in self.loggers:
logger.info(func.upper() + ": " + msg)
def debug(self, msg, func=""):
for logger in self.loggers:
logger.debug(func.upper() + ": " + msg)
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

+101
View File
@@ -0,0 +1,101 @@
# general imports
import os
import time
import numpy as np
import datetime
"""
All classes are ranked in their hierarchy, like this (ranks of the US Marines):
Capt << BGen << MajGen << LtGen << Gen
"""
# imports from project
import RaspberryPie.tasks as tasks
import RaspberryPie.hardwareControl as hardwareControl
import RaspberryPie.config as config
import RaspberryPie.logger as logger
import RaspberryPie.internetHandling as internetHandling
captScribe = logger.CaptScribe(config.config.config["File Locations"]["logfile_info"], config.config.config["File Locations"]["logfile_error"])
internetHandling._set_captScribe(captScribe)
hardwareControl._set_captScribe(captScribe)
ltGenInterpreter, genScheduler = None
tasks = [
tasks.Task(hardwareControl.majGenGPIOController.open, lambda msg: 1 if ("auf" == msg.subject.lower() or "open" == msg.subject.lower()) else 0),
tasks.Task(hardwareControl.majGenGPIOController.close, lambda msg: 1 if ("zu" == msg.subject.lower() or "close" == msg.subject.lower()) else 0)
]
class LtGenInterpreter:
def __init__(self, tasks: list, communicator: internetHandling.MajGenApiCom, schedule_timing=None):
if not schedule_timing:
schedule_timing = config.config.config["Timing"]["maildelta"]
self.schedule_timing
self.tasks = tasks
self.communicator = communicator
def get_schedule(self):
return self.schedule_timing
def execute(self):
messages = self.communicator.get_emails()
for message in messages:
for task in self.tasks:
if task.test(message):
task.function()
class GenScheduler:
def __init__(self, subordinates: list):
self.subordinates = subordinates
self.schedule = {}
self.schedule_delta = {}
for subordinate in self.subordinates:
self.schedule[subordinate] = list(datetime.datetime.now())
self.schedule_delta[subordinate] = list(subordinate.get_schedule())
self.time = datetime.datetime.now
def execute(self):
while True:
try:
for subordinate in self.schedule:
if self.schedule[subordinate][np.argmin(self.schedule[subordinate])] <= self.time():
subordinate.execute()
self.schedule[subordinate][np.argmin(self.schedule[subordinate])] += self.schedule_delta[subordinate][np.argmin(self.schedule[subordinate])]
time_list = lambda: [self.schedule[subordinate][np.argmin(self.schedule[subordinate])] for subordinate in self.schedule]
sleep_duration = time_list()[np.argmin(time_list())]
time.sleep(sleep_duration)
except Exception as e:
captScribe.critical("A not expected error occured: {}".format(str(e)), "GenScheduler.execute")
def initiate_shutdown(self, reboot=True):
hardwareControl.majGenGPIOController.finalMission()
reboot_str = lambda: " -r" if reboot else ""
os.system("shutdown{} 0".format(reboot_str()))
def init():
global ltGenInterpreter, genScheduler
ltGenInterpreter = LtGenInterpreter(tasks, internetHandling.majGenApiCom)
genScheduler = GenScheduler((ltGenInterpreter, hardwareControl.majGenVisualRecorder, hardwareControl.majGenAirRecoder))
def main():
suspend_shutdown = False
if genScheduler is GenScheduler:
try:
genScheduler.execute()
except Exception as e:
captScribe.critical(str(e), "main()")
if e is KeyboardInterrupt:
suspend_shutdown = True
finally:
if not suspend_shutdown:
genScheduler.initiate_shutdown()
print("The programm has finished. For more information see the log files.")
init()
main()
+7
View File
@@ -0,0 +1,7 @@
requests==2.22.0
numpy==1.18.4
Adafruit_DHT==1.4.0
dropbox==10.4.1
html2text==2020.1.16
picamera==1.13
RPi.GPIO==0.7.0
+8
View File
@@ -0,0 +1,8 @@
from RaspberryPie.internetHandling import MajGenApiCom
class tasks.Task:
def __init__(self, function, check):
self.function = function
self.check = check
def test(self, email: MajGenApiCom.Telegram):
return self.check(email)
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RaspberryPie</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.js" integrity="sha512-G8JE1Xbr0egZE5gNGyUm1fF764iHVfRXshIoUWCTPAbKkkItp/6qal5YAHXrxEu4HNfPTQs6HOu3D5vCGS1j3w==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js" integrity="sha512-vBmx0N/uQOXznm/Nbkp7h0P1RfLSj0HQrFSzV8m7rOGyj30fYAOKHYvCNez+yM8IrfnW0TCodDEjRqf6fodf/Q==" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.css" integrity="sha512-SUJFImtiT87gVCOXl3aGC00zfDl6ggYAw5+oheJvRJ8KBXZrr/TMISSdVJ5bBarbQDRC2pR5Kto3xTR0kpZInA==" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css" integrity="sha512-/zs32ZEJh+/EO2N1b0PEdoA10JkdC3zJ8L5FTiQu82LR9S/rOQNfQN7U59U9BC12swNeRAz3HSzIL2vpp4fv3w==" crossorigin="anonymous" />
<script src="https://raw.githubusercontent.com/lade043/RaspberryPie/master/RaspberryPie/js/main.js"></script>
</head>
<body>
<h1>RaspberryPie</h1>
<h2>Current Weather</h2>
<p><a id="WeatherTime">{{ WeatherTime }}</a><br>Temperature: <a id="Temperature">{{ temp }}</a>, Humidity: <a id="Humidity">{{ humi }}</a></p>
<canvas id="EnvironmentChart" width="400" height="200"></canvas>
<h2>Weather Forecast</h2>
<canvas id="ForecastChart" width="400" height="200"></canvas>
<h2>Third party weather providers</h2>
{% for image in weather_urls %}
<img src="{{ image }}">
<h2>Webcam</h2>
<a id="PictureTime">{{ PictureTime }}</a><br>
<img id="Webcam" src="data:image/jpg;base64,{{ picture }}">
</body>
</html>