Разработаны модули извлечения и сохранения статистических данных, разработан набросок веб-приложения

This commit is contained in:
2024-07-26 02:45:25 +03:00
commit 9ba660f803
14 changed files with 977 additions and 0 deletions

BIN
OpenHardwareMonitorLib.dll Normal file

Binary file not shown.

1
database/__init__.py Normal file
View File

@ -0,0 +1 @@
from .main import connect, get_sensors, get_sensor_values

190
database/main.py Normal file
View File

@ -0,0 +1,190 @@
import os
import sqlite3
from models import HardwareType, Hardware, SensorType, Sensor, SensorValue
directory_path = os.path.join(
os.getenv('appdata'),
'csasq',
'Hardware Monitor',
)
file_path = os.path.join(
directory_path,
'data.sql',
)
def connect():
os.makedirs(
name=directory_path,
exist_ok=True,
)
return sqlite3.connect(
database=file_path,
)
with connect() as connection:
cursor = connection.cursor()
sql = '''
create table if not exists settings (
name,
value,
primary key (name)
);
create table if not exists hardware_types (
id integer,
name,
primary key (id),
unique (name)
);
create table if not exists hardware (
id integer,
identifier,
name,
hardware_type_id,
primary key (id),
foreign key (hardware_type_id) references hardware_types on delete cascade on update cascade,
unique (identifier)
);
create table if not exists sensor_types (
id integer,
name,
primary key (id),
unique (name)
);
create table if not exists sensors (
id integer,
hardware_id,
sensor_type_id,
identifier,
name,
'index',
is_default_hidden,
enabled,
primary key (id),
foreign key (hardware_id) references hardware on delete cascade on update cascade,
foreign key (sensor_type_id) references sensor_types on delete cascade on update cascade,
unique (identifier, sensor_type_id)
);
create table if not exists sensor_values (
sensor_id,
timestamp,
value,
foreign key (sensor_id) references sensors
);
pragma foreign_keys = on;
'''
cursor.executescript(sql)
def get_sensors() -> list[Sensor]:
with connect() as connection:
cursor = connection.cursor()
sql = '''
select
sensors.id,
hardware.id,
hardware.identifier,
hardware.name,
hardware_types.id,
hardware_types.name,
sensor_types.id,
sensor_types.name,
sensors.identifier,
sensors.name,
sensors.'index',
sensors.is_default_hidden,
sensors.enabled
from
sensors
left outer join hardware on
sensors.hardware_id = hardware.id
left outer join hardware_types on
hardware.hardware_type_id = hardware_types.id
left outer join sensor_types on
sensors.sensor_type_id = sensor_types.id;
'''
cursor.execute(sql)
records = cursor.fetchall()
return list(
Sensor(
id=sensor_id,
hardware=Hardware(
id=sensor_hardware_id,
identifier=sensor_hardware_identifier,
name=sensor_hardware_name,
hardware_type=HardwareType(
id=sensor_hardware_type_id,
name=sensor_hardware_type_name,
),
),
sensor_type=SensorType(
id=sensor_type_id,
name=sensor_type_name,
),
identifier=sensor_identifier,
name=sensor_name,
index=sensor_index,
is_default_hidden=sensor_is_default_hidden,
enabled=sensor_enabled,
)
for
sensor_id,
sensor_hardware_id,
sensor_hardware_identifier,
sensor_hardware_name,
sensor_hardware_type_id,
sensor_hardware_type_name,
sensor_type_id,
sensor_type_name,
sensor_identifier,
sensor_name,
sensor_index,
sensor_is_default_hidden,
sensor_enabled,
in
records
)
def get_sensor_values() -> list[SensorValue]:
with connect() as connection:
cursor = connection.cursor()
sql = '''
select
sensor_values.sensor_id,
sensor_values.timestamp,
sensor_values.value
from
sensor_values
where
sensor_values.sensor_id in ?;
'''
cursor.execute(
sql,
(
[15, 16, 17],
),
)
records, = cursor.fetchall()
return list(
SensorValue(
sensor_id=sensor_id,
timestamp=timestamp,
value=value,
)
for
sensor_id,
timestamp,
value,
in
records
)

15
main.py Normal file
View File

@ -0,0 +1,15 @@
from monitor import Monitor
from threading import Thread
import uvicorn
from web import app
Monitor().start()
web_thread = Thread(
target=uvicorn.run(
app=app,
host='localhost',
port=8000,
),
)
web_thread.start()

1
models/__init__.py Normal file
View File

@ -0,0 +1 @@
from .main import Setting, HardwareType, Hardware, SensorType, Sensor, SensorValue

53
models/main.py Normal file
View File

@ -0,0 +1,53 @@
from datetime import datetime
from pydantic import BaseModel
class Setting(BaseModel):
name: str
value: str
class HardwareType(BaseModel):
id: int
name: str
def __hash__(self):
return self.id
class Hardware(BaseModel):
id: int
identifier: str
name: str
hardware_type: HardwareType
def __hash__(self):
return self.id
class SensorType(BaseModel):
id: int
name: str
def __hash__(self):
return self.id
class Sensor(BaseModel):
id: int
hardware: Hardware
sensor_type: SensorType
identifier: str
name: str
index: int
is_default_hidden: bool
enabled: bool
def __hash__(self):
return self.id
class SensorValue(BaseModel):
sensor: Sensor
timestamp: datetime
value: str

1
monitor/__init__.py Normal file
View File

@ -0,0 +1 @@
from .main import Monitor

159
monitor/main.py Normal file
View File

@ -0,0 +1,159 @@
import clr
from datetime import datetime
import os
from threading import Thread
import time
import database
class Monitor(Thread):
def __init__(self):
clr.AddReference(r'OpenHardwareMonitorLib')
from OpenHardwareMonitor import Hardware
self.handle = Hardware.Computer()
self.handle.MainboardEnabled = True
self.handle.CPUEnabled = True
self.handle.RAMEnabled = True
self.handle.GPUEnabled = True
self.handle.HDDEnabled = True
self.handle.Open()
super().__init__()
def run(self):
def parse_sensor(sensor):
sql = '''
insert into sensor_types (
name
) values (
?
)
on conflict (name) do update set
name = sensor_types.name
returning
sensor_types.id;
'''
cursor.execute(
sql,
(
str(sensor.SensorType),
),
)
sensor_type_id, = cursor.fetchone()
sql = '''
insert into sensors (
hardware_id,
sensor_type_id,
identifier,
name,
'index',
is_default_hidden,
enabled
) values (
?,
?,
?,
?,
?,
?,
false
)
on conflict (identifier, sensor_type_id) do update set
hardware_id = sensors.hardware_id,
name = sensors.name,
'index' = sensors.'index',
is_default_hidden = sensors.is_default_hidden,
enabled = sensors.enabled
returning
sensors.id,
sensors.enabled;
'''
cursor.execute(
sql,
(
str(hardware_id),
sensor_type_id,
str(sensor.Identifier),
str(sensor.Name),
int(sensor.Index),
bool(sensor.IsDefaultHidden),
),
)
sensor_id, sensor_enabled, = cursor.fetchone()
if not sensor_enabled:
return
sql = '''
insert into sensor_values (
sensor_id,
timestamp,
value
) values (
?,
?,
?
);
'''
cursor.execute(
sql,
(
sensor_id,
datetime.now(),
sensor.Value,
),
)
while True:
for hardware in self.handle.Hardware:
with database.connect() as connection:
cursor = connection.cursor()
sql = '''
insert into hardware_types (
name
) values (
?
)
on conflict (name) do update set
name = hardware_types.name
returning
hardware_types.id;
'''
cursor.execute(
sql,
(
str(hardware.HardwareType),
),
)
hardware_type_id, = cursor.fetchone()
sql = '''
insert into hardware (
identifier,
name,
hardware_type_id
) values (
?,
?,
?
)
on conflict (identifier) do update set
name = hardware.name,
hardware_type_id = hardware.hardware_type_id
returning
hardware.id;
'''
cursor.execute(
sql,
(
str(hardware.Identifier),
str(hardware.Name),
hardware_type_id,
),
)
hardware_id, = cursor.fetchone()
hardware.Update()
for sensor in hardware.Sensors:
parse_sensor(sensor)
for subhardware in hardware.SubHardware:
subhardware.Update()
for subsensor in subhardware.Sensors:
parse_sensor(subsensor)
time.sleep(1)

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
fastapi
# gunicorn
jinja2
pydantic
pythonnet
uvicorn

276
templates/main.jinja2 Normal file
View File

@ -0,0 +1,276 @@
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<title></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.min.css" integrity="sha256-BicZsQAhkGHIoR//IB2amPN5SrRb3fHB8tFsnqRAwnk=" crossorigin="anonymous" />
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd.min.js"></script>
<style>
html {
min-height: 100vh !important;
}
body,
main {
min-height: 100% !important;
}
svg > symbol {
fill: silver;
}
.bi {
display: inline-block;
width: 1rem;
height: 1rem;
}
/*
* Sidebar
*/
@media (min-width: 768px) {
.sidebar .offcanvas-lg {
position: -webkit-sticky;
position: sticky;
top: 48px;
}
.navbar-search {
display: block;
}
}
.sidebar .nav-link {
font-size: .875rem;
font-weight: 500;
}
.sidebar .nav-link.active {
color: #2470dc;
}
.sidebar-heading {
font-size: .75rem;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .form-control {
padding: .75rem 1rem;
}
</style>
<script >
const api_url = new URL(location.origin);
</script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="check2" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"></path>
</symbol>
<symbol id="circle-half" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"></path>
</symbol>
<symbol id="moon-stars-fill" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"></path>
</symbol>
<symbol id="sun-fill" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path>
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="calendar3" viewBox="0 0 16 16">
<path d="M14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM1 3.857C1 3.384 1.448 3 2 3h12c.552 0 1 .384 1 .857v10.286c0 .473-.448.857-1 .857H2c-.552 0-1-.384-1-.857V3.857z"></path>
<path d="M6.5 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
</symbol>
<symbol id="cart" viewBox="0 0 16 16">
<path d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .49.598l-1 5a.5.5 0 0 1-.465.401l-9.397.472L4.415 11H13a.5.5 0 0 1 0 1H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM3.102 4l.84 4.479 9.144-.459L13.89 4H3.102zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"></path>
</symbol>
<symbol id="chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"></path>
</symbol>
<symbol id="door-closed" viewBox="0 0 16 16">
<path d="M3 2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v13h1.5a.5.5 0 0 1 0 1h-13a.5.5 0 0 1 0-1H3V2zm1 13h8V2H4v13z"></path>
<path d="M9 9a1 1 0 1 0 2 0 1 1 0 0 0-2 0z"></path>
</symbol>
<symbol id="file-earmark" viewBox="0 0 16 16">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"></path>
</symbol>
<symbol id="file-earmark-text" viewBox="0 0 16 16">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"></path>
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"></path>
</symbol>
<symbol id="gear-wide-connected" viewBox="0 0 16 16">
<path d="M7.068.727c.243-.97 1.62-.97 1.864 0l.071.286a.96.96 0 0 0 1.622.434l.205-.211c.695-.719 1.888-.03 1.613.931l-.08.284a.96.96 0 0 0 1.187 1.187l.283-.081c.96-.275 1.65.918.931 1.613l-.211.205a.96.96 0 0 0 .434 1.622l.286.071c.97.243.97 1.62 0 1.864l-.286.071a.96.96 0 0 0-.434 1.622l.211.205c.719.695.03 1.888-.931 1.613l-.284-.08a.96.96 0 0 0-1.187 1.187l.081.283c.275.96-.918 1.65-1.613.931l-.205-.211a.96.96 0 0 0-1.622.434l-.071.286c-.243.97-1.62.97-1.864 0l-.071-.286a.96.96 0 0 0-1.622-.434l-.205.211c-.695.719-1.888.03-1.613-.931l.08-.284a.96.96 0 0 0-1.186-1.187l-.284.081c-.96.275-1.65-.918-.931-1.613l.211-.205a.96.96 0 0 0-.434-1.622l-.286-.071c-.97-.243-.97-1.62 0-1.864l.286-.071a.96.96 0 0 0 .434-1.622l-.211-.205c-.719-.695-.03-1.888.931-1.613l.284.08a.96.96 0 0 0 1.187-1.186l-.081-.284c-.275-.96.918-1.65 1.613-.931l.205.211a.96.96 0 0 0 1.622-.434l.071-.286zM12.973 8.5H8.25l-2.834 3.779A4.998 4.998 0 0 0 12.973 8.5zm0-1a4.998 4.998 0 0 0-7.557-3.779l2.834 3.78h4.723zM5.048 3.967c-.03.021-.058.043-.087.065l.087-.065zm-.431.355A4.984 4.984 0 0 0 3.002 8c0 1.455.622 2.765 1.615 3.678L7.375 8 4.617 4.322zm.344 7.646.087.065-.087-.065z"></path>
</symbol>
<symbol id="graph-up" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M0 0h1v15h15v1H0V0Zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07Z"></path>
</symbol>
<symbol id="house-fill" viewBox="0 0 16 16">
<path d="M8.707 1.5a1 1 0 0 0-1.414 0L.646 8.146a.5.5 0 0 0 .708.708L8 2.207l6.646 6.647a.5.5 0 0 0 .708-.708L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.707 1.5Z"></path>
<path d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6Z"></path>
</symbol>
<symbol id="list" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path>
</symbol>
<symbol id="people" viewBox="0 0 16 16">
<path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8Zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022ZM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816ZM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275ZM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"></path>
</symbol>
<symbol id="plus-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</symbol>
<symbol id="puzzle" viewBox="0 0 16 16">
<path d="M3.112 3.645A1.5 1.5 0 0 1 4.605 2H7a.5.5 0 0 1 .5.5v.382c0 .696-.497 1.182-.872 1.469a.459.459 0 0 0-.115.118.113.113 0 0 0-.012.025L6.5 4.5v.003l.003.01c.004.01.014.028.036.053a.86.86 0 0 0 .27.194C7.09 4.9 7.51 5 8 5c.492 0 .912-.1 1.19-.24a.86.86 0 0 0 .271-.194.213.213 0 0 0 .039-.063v-.009a.112.112 0 0 0-.012-.025.459.459 0 0 0-.115-.118c-.375-.287-.872-.773-.872-1.469V2.5A.5.5 0 0 1 9 2h2.395a1.5 1.5 0 0 1 1.493 1.645L12.645 6.5h.237c.195 0 .42-.147.675-.48.21-.274.528-.52.943-.52.568 0 .947.447 1.154.862C15.877 6.807 16 7.387 16 8s-.123 1.193-.346 1.638c-.207.415-.586.862-1.154.862-.415 0-.733-.246-.943-.52-.255-.333-.48-.48-.675-.48h-.237l.243 2.855A1.5 1.5 0 0 1 11.395 14H9a.5.5 0 0 1-.5-.5v-.382c0-.696.497-1.182.872-1.469a.459.459 0 0 0 .115-.118.113.113 0 0 0 .012-.025L9.5 11.5v-.003a.214.214 0 0 0-.039-.064.859.859 0 0 0-.27-.193C8.91 11.1 8.49 11 8 11c-.491 0-.912.1-1.19.24a.859.859 0 0 0-.271.194.214.214 0 0 0-.039.063v.003l.001.006a.113.113 0 0 0 .012.025c.016.027.05.068.115.118.375.287.872.773.872 1.469v.382a.5.5 0 0 1-.5.5H4.605a1.5 1.5 0 0 1-1.493-1.645L3.356 9.5h-.238c-.195 0-.42.147-.675.48-.21.274-.528.52-.943.52-.568 0-.947-.447-1.154-.862C.123 9.193 0 8.613 0 8s.123-1.193.346-1.638C.553 5.947.932 5.5 1.5 5.5c.415 0 .733.246.943.52.255.333.48.48.675.48h.238l-.244-2.855zM4.605 3a.5.5 0 0 0-.498.55l.001.007.29 3.4A.5.5 0 0 1 3.9 7.5h-.782c-.696 0-1.182-.497-1.469-.872a.459.459 0 0 0-.118-.115.112.112 0 0 0-.025-.012L1.5 6.5h-.003a.213.213 0 0 0-.064.039.86.86 0 0 0-.193.27C1.1 7.09 1 7.51 1 8c0 .491.1.912.24 1.19.07.14.14.225.194.271a.213.213 0 0 0 .063.039H1.5l.006-.001a.112.112 0 0 0 .025-.012.459.459 0 0 0 .118-.115c.287-.375.773-.872 1.469-.872H3.9a.5.5 0 0 1 .498.542l-.29 3.408a.5.5 0 0 0 .497.55h1.878c-.048-.166-.195-.352-.463-.557-.274-.21-.52-.528-.52-.943 0-.568.447-.947.862-1.154C6.807 10.123 7.387 10 8 10s1.193.123 1.638.346c.415.207.862.586.862 1.154 0 .415-.246.733-.52.943-.268.205-.415.39-.463.557h1.878a.5.5 0 0 0 .498-.55l-.001-.007-.29-3.4A.5.5 0 0 1 12.1 8.5h.782c.696 0 1.182.497 1.469.872.05.065.091.099.118.115.013.008.021.01.025.012a.02.02 0 0 0 .006.001h.003a.214.214 0 0 0 .064-.039.86.86 0 0 0 .193-.27c.14-.28.24-.7.24-1.191 0-.492-.1-.912-.24-1.19a.86.86 0 0 0-.194-.271.215.215 0 0 0-.063-.039H14.5l-.006.001a.113.113 0 0 0-.025.012.459.459 0 0 0-.118.115c-.287.375-.773.872-1.469.872H12.1a.5.5 0 0 1-.498-.543l.29-3.407a.5.5 0 0 0-.497-.55H9.517c.048.166.195.352.463.557.274.21.52.528.52.943 0 .568-.447.947-.862 1.154C9.193 5.877 8.613 6 8 6s-1.193-.123-1.638-.346C5.947 5.447 5.5 5.068 5.5 4.5c0-.415.246-.733.52-.943.268-.205.415-.39.463-.557H4.605z"></path>
</symbol>
<symbol id="search" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"></path>
</symbol>
<symbol id="cpu-icon" viewBox="0 0 16 16">
<path d="M5 0a.5.5 0 0 1 .5.5V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2A2.5 2.5 0 0 1 14 4.5h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14a2.5 2.5 0 0 1-2.5 2.5v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14A2.5 2.5 0 0 1 2 11.5H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2A2.5 2.5 0 0 1 4.5 2V.5A.5.5 0 0 1 5 0zm-.5 3A1.5 1.5 0 0 0 3 4.5v7A1.5 1.5 0 0 0 4.5 13h7a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 11.5 3h-7zM5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3zM6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"></path>
</symbol>
<symbol id="gpu-icon" viewBox="0 0 16 16">
<path d="M4 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Zm7.5-1.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"></path>
<path d="M0 1.5A.5.5 0 0 1 .5 1h1a.5.5 0 0 1 .5.5V4h13.5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5H2v2.5a.5.5 0 0 1-1 0V2H.5a.5.5 0 0 1-.5-.5Zm5.5 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM9 8a2.5 2.5 0 1 0 5 0 2.5 2.5 0 0 0-5 0Z"></path>
<path d="M3 12.5h3.5v1a.5.5 0 0 1-.5.5H3.5a.5.5 0 0 1-.5-.5v-1Zm4 1v-1h4v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5Z"></path>
</symbol>
<symbol id="ram-icon" viewBox="0 0 16 16">
<path d="M1 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4.586a1 1 0 0 0 .707-.293l.353-.353a.5.5 0 0 1 .708 0l.353.353a1 1 0 0 0 .707.293H15a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H1Zm.5 1h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm5 0h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm4.5.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4ZM2 10v2H1v-2h1Zm2 0v2H3v-2h1Zm2 0v2H5v-2h1Zm3 0v2H8v-2h1Zm2 0v2h-1v-2h1Zm2 0v2h-1v-2h1Zm2 0v2h-1v-2h1Z"></path>
</symbol>
<symbol id="rom-icon" viewBox="0 0 16 16">
<path d="M4.5 11a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zM3 10.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"></path>
<path d="M16 11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V9.51c0-.418.105-.83.305-1.197l2.472-4.531A1.5 1.5 0 0 1 4.094 3h7.812a1.5 1.5 0 0 1 1.317.782l2.472 4.53c.2.368.305.78.305 1.198V11zM3.655 4.26 1.592 8.043C1.724 8.014 1.86 8 2 8h12c.14 0 .276.014.408.042L12.345 4.26a.5.5 0 0 0-.439-.26H4.094a.5.5 0 0 0-.44.26zM1 10v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1z"></path>
</symbol>
<symbol id="mainboard-icon" viewBox="0 0 16 16">
<path d="M11.5 2a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5Zm2 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5Zm-10 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm0 2a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM5 3a1 1 0 0 0-1 1h-.5a.5.5 0 0 0 0 1H4v1h-.5a.5.5 0 0 0 0 1H4a1 1 0 0 0 1 1v.5a.5.5 0 0 0 1 0V8h1v.5a.5.5 0 0 0 1 0V8a1 1 0 0 0 1-1h.5a.5.5 0 0 0 0-1H9V5h.5a.5.5 0 0 0 0-1H9a1 1 0 0 0-1-1v-.5a.5.5 0 0 0-1 0V3H6v-.5a.5.5 0 0 0-1 0V3Zm0 1h3v3H5V4Zm6.5 7a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h2a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-2Z"></path>
<path d="M1 2a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-2H.5a.5.5 0 0 1-.5-.5v-1A.5.5 0 0 1 .5 9H1V8H.5a.5.5 0 0 1-.5-.5v-1A.5.5 0 0 1 .5 6H1V5H.5a.5.5 0 0 1-.5-.5v-2A.5.5 0 0 1 .5 2H1Zm1 11a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v11Z"></path>
</symbol>
</svg>
<header class="navbar sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6 text-white" href="#">Hardware Monitor</a>
<ul class="navbar-nav flex-row d-md-none">
<li class="nav-item text-nowrap">
<button class="nav-link px-3 text-white" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSearch" aria-controls="navbarSearch" aria-expanded="false"
aria-label="Toggle search">
<svg class="bi">
<use xlink:href="#search"></use>
</svg>
</button>
</li>
<li class="nav-item text-nowrap">
<button class="nav-link px-3 text-white" type="button" data-bs-toggle="offcanvas"
data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false"
aria-label="Toggle navigation">
<svg class="bi">
<use xlink:href="#list"></use>
</svg>
</button>
</li>
</ul>
<div id="navbarSearch" class="navbar-search w-100 collapse">
<input class="form-control w-100 rounded-0 border-0" type="text" placeholder="Search" aria-label="Search">
</div>
</header>
<div class="container-fluid">
<div class="row">
<div class="sidebar border border-right col-md-3 col-lg-2 p-0 bg-body-tertiary">
<div class="offcanvas-lg offcanvas-end bg-body-tertiary" tabindex="-1" id="sidebarMenu"
aria-labelledby="sidebarMenuLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="sidebarMenuLabel">Hardware Monitor</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#sidebarMenu" aria-label="Close"></button>
</div>
<div class="offcanvas-body d-md-flex flex-column p-0 pt-lg-3 pb-lg-3 overflow-y-auto">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2 active" aria-current="page" href="/reports/mainboard">
<svg class="bi">
<use xlink:href="#mainboard-icon"></use>
</svg>
Mainboard
</a>
</li>
</ul>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2" aria-current="page" href="/reports/cpu">
<svg class="bi">
<use xlink:href="#cpu-icon"></use>
</svg>
CPU
</a>
</li>
</ul>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2" aria-current="page" href="/reports/gpu">
<svg class="bi">
<use xlink:href="#gpu-icon"></use>
</svg>
GPU
</a>
</li>
</ul>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2" aria-current="page" href="/reports/ram">
<svg class="bi">
<use xlink:href="#ram-icon"></use>
</svg>
RAM
</a>
</li>
</ul>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2" aria-current="page" href="/reports/rom">
<svg class="bi">
<use xlink:href="#rom-icon"></use>
</svg>
ROM
</a>
</li>
</ul>
<hr class="my-3">
<ul class="nav flex-column mb-auto">
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2" href="/settings">
<svg class="bi">
<use xlink:href="#gear-wide-connected"></use>
</svg>
Настройки
</a>
</li>
</ul>
</div>
</div>
</div>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
{% block main %}
{% endblock %}
</main>
</div>
</div>
</body>
</html>

99
templates/reports.jinja2 Normal file
View File

@ -0,0 +1,99 @@
{% extends 'main.jinja2' %}
{% block main %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Mainboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
</div>
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle d-flex align-items-center gap-1" data-bs-toggle="dropdown">
<svg class="bi">
<use xlink:href="#calendar3"></use>
</svg>
Температура
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">День</a></li>
<li><a class="dropdown-item" href="#">Неделя</a></li>
<li><a class="dropdown-item" href="#">Месяц</a></li>
</ul>
</div>
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle d-flex align-items-center gap-1" data-bs-toggle="dropdown">
<svg class="bi">
<use xlink:href="#calendar3"></use>
</svg>
День
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">День</a></li>
<li><a class="dropdown-item" href="#">Неделя</a></li>
<li><a class="dropdown-item" href="#">Месяц</a></li>
</ul>
</div>
</div>
</div>
<canvas class="my-4 w-100" id="chart" width="1537" height="649" style="display: block; box-sizing: border-box; height: 649px; width: 1537px;"></canvas>
<script>
const canvas = document.querySelector('#chart');
const DATA_COUNT = 12;
const labels = [];
for (let i = 0; i < DATA_COUNT; ++i) {
labels.push(i.toString());
}
const datapoints = [0, 20, 20, 60, 60, 120, 180, 120, 125, 105, 110, 170];
const data = {
labels: labels,
datasets: [
{
label: 'Cubic interpolation',
data: datapoints,
borderColor: 'blue',
fill: false,
tension: .4,
},
],
};
const config = {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: false,
{#text: 'Chart.js Line Chart'#}
},
},
scales: {
y: {
stacked: true,
grid: {
display: true,
color: 'rgba(255, 255, 255, .2)',
},
},
x: {
stacked: true,
grid: {
display: true,
color: 'rgba(255, 255, 255, .2)',
},
},
},
},
};
const chart = new Chart(
canvas.getContext('2d'),
config,
);
</script>
{% endblock %}

79
templates/settings.jinja2 Normal file
View File

@ -0,0 +1,79 @@
{% extends 'main.jinja2' %}
{% block main %}
<div class="pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Настройки</h1>
</div>
{# <h1 class="h4">Панель управления</h1>#}
{# <div class="input-group mb-3">#}
{# <span class="input-group-text">http://</span>#}
{# <input type="text" class="form-control" id="hostname" value="{{ settings['hostname'] }}" />#}
{# <span class="input-group-text">:</span>#}
{# <input type="number" class="form-control" id="port" min="1024" max="65535" value="{{ settings['port'] }}" />#}
{# </div>#}
<section>
<h1 class="h4">Датчики</h1>
<div id="sensors">
{% for hardware_type in settings['hardware_types'] %}
<div class="mb-3 form-check" data-hardware-type-id="{{ hardware_type.id }}">
<input type="checkbox" class="form-check-input" id="hardware-type-{{ hardware_type.id }}" />
<label class="form-check-label" for="hardware-type-{{ hardware_type.id }}">{{ hardware_type.name }}</label>
</div>
{% for hardware in settings['hardware_types'][hardware_type] %}
<div class="ms-5">
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="hardware-{{ hardware.id }}" data-hardware-id="{{ hardware.id }}" />
<label class="form-check-label" for="hardware-{{ hardware.id }}">{{ hardware.name }} <small class="text-secondary">{{ hardware.identifier }}</small></label>
</div>
{% for sensor_type in settings['hardware_types'][hardware_type][hardware] %}
<div class="ms-5">
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="sensor-type-{{ sensor_type.id }}" data-sensor-type-id="{{ sensor_type.id }}" />
<label class="form-check-label" for="sensor-type-{{ sensor_type.id }}">{{ sensor_type.name }}</label>
</div>
{% for sensor in settings['hardware_types'][hardware_type][hardware][sensor_type] %}
<div class="ms-5">
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="sensor-{{ sensor.id }}" data-sensor-id="{{ sensor.id }}" {% if sensor.enabled %} checked {% endif %} />
<label class="form-check-label" for="sensor-{{ sensor.id }}">{{ sensor.name }} <small class="text-secondary">{{ sensor.identifier }}</small></label>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endfor %}
{% endfor %}
</div>
</section>
<script>
</script>
{# <script>#}
{# const $sensors = document.querySelector('#sensors');#}
{# const xhr = new XMLHttpRequest();#}
{# api_url.pathname = '/api/sensors';#}
{# xhr.open('get', api_url);#}
{# xhr.onload = () => {#}
{# if (xhr.status !== 200) return;#}
{# const sensors = JSON.parse(xhr.response);#}
{# for (const sensor of sensors) {#}
{# if ($sensors.querySelector(`[data-hardware-type-id="${ sensor.hardware.hardware_type.id }"]`) === null) {#}
{# let $div = document.createElement('div');#}
{# $div.classList.add('mb-3', 'form-check');#}
{# let $input = document.createElement('input');#}
{# $input.type = 'checkbox';#}
{# $input.classList.add('form-check-input');#}
{# $input.id = `hardware-type-${ sensor.hardware.hardware_type.id }`;#}
{# $input.dataset.hardwareTypeId = String(sensor.hardware.hardware_type.id);#}
{# let $label = document.createElement('label');#}
{# $label.classList.add('form-check-label');#}
{# $label.setAttribute('for', $input.id);#}
{# $label.textContent = sensor.hardware.hardware_type.name;#}
{# $div.append($input, $label);#}
{# $sensors.append($div);#}
{# }#}
{# #}
{# }#}
{# };#}
{# xhr.send();#}
{# </script>#}
{% endblock %}

1
web/__init__.py Normal file
View File

@ -0,0 +1 @@
from .main import app

96
web/main.py Normal file
View File

@ -0,0 +1,96 @@
import asyncio
from fastapi import FastAPI, Request, Header, Body, Path
from fastapi.responses import Response, HTMLResponse, FileResponse, RedirectResponse, JSONResponse
from fastapi.exceptions import HTTPException
from fastapi.staticfiles import StaticFiles
from jinja2 import Environment, FileSystemLoader
import os
import database
import models
# from models import MailSizeData
app = FastAPI()
app.mount(
path='/static',
app=StaticFiles(
directory=os.path.join(
os.getcwd(),
'static',
),
),
)
env = Environment(
loader=FileSystemLoader('./templates'),
enable_async=True,
)
@app.get('/')
async def function():
return RedirectResponse(
url='/reports/mainboard',
status_code=307,
)
@app.get('/reports/mainboard')
async def function():
template = env.get_template('reports.jinja2')
return HTMLResponse(
content=await template.render_async(),
status_code=200,
)
@app.get('/settings')
async def function():
hardware_types = dict()
sensors = database.get_sensors()
# hardware_types = set(
# sensor.hardware.hardware_type
# for sensor in sensors
# )
for hardware_type in set(
sensor.hardware.hardware_type
for sensor in sensors
):
hardware_types[hardware_type] = dict()
for hardware in set(
sensor.hardware
for sensor in sensors
if sensor.hardware.hardware_type == hardware_type
):
hardware_types[hardware_type][hardware] = dict()
for sensor_type in set(
sensor.sensor_type
for sensor in sensors
if sensor.hardware == hardware
):
hardware_types[hardware_type][hardware][sensor_type] = list()
for sensor in sensors:
if sensor.hardware == hardware and sensor.sensor_type == sensor_type:
hardware_types[hardware_type][hardware][sensor_type].append(sensor)
template = env.get_template('settings.jinja2')
return HTMLResponse(
content=await template.render_async(
settings={
'hostname': 'localhost',
'port': 8000,
'hardware_types': hardware_types,
},
),
status_code=200,
)
@app.get('/api/sensors')
async def function():
return database.get_sensors()
@app.get('/api/sensor-values')
async def function():
return database.get_sensor_values()