Merge pull request #1 from ferdzo/refactor

Big refactor
This commit is contained in:
2024-10-10 00:11:18 +02:00
committed by GitHub
26 changed files with 708 additions and 349 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

54
iotDashboard/db_create.py Normal file
View File

@@ -0,0 +1,54 @@
import psycopg2
from psycopg2 import sql
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Define your database connection parameters
DATABASE_NAME = os.getenv('DB_NAME', 'example')
USER = os.getenv('DB_USER', 'postgres')
PASSWORD = os.getenv('DB_PASSWORD', 'coolermaster')
HOST = os.getenv('DB_HOST', '10.10.0.1')
PORT = os.getenv('DB_PORT', '5555')
def create_sensor_readings_table():
"""Create the sensor_readings table if it does not exist."""
try:
# Establish connection to the database
conn = psycopg2.connect(
dbname=DATABASE_NAME,
user=USER,
password=PASSWORD,
host=HOST,
port=PORT
)
with conn.cursor() as cursor:
# SQL command to create the sensor_readings table
create_table_query = """
CREATE TABLE IF NOT EXISTS sensor_readings (
time TIMESTAMPTZ NOT NULL,
device_name VARCHAR(255) NOT NULL, -- Use device_name as a string
metric VARCHAR(50) NOT NULL, -- Type of sensor
value DOUBLE PRECISION NOT NULL, -- The sensor's value
PRIMARY KEY (time, device_name, metric) -- Composite primary key
);
"""
cursor.execute(create_table_query)
print("Table 'sensor_readings' created or already exists.")
# Commit changes
conn.commit()
except Exception as e:
print(f"Error during database operations: {e}")
finally:
if conn:
conn.close()
print("Database connection closed.")
if __name__ == "__main__":
create_sensor_readings_table()

View File

@@ -1,7 +1,72 @@
from django import forms
from .models import Device
from .models import Device, Sensor, SensorType
class DeviceForm(forms.ModelForm):
# Optionally include sensors as choices in the form if relevant
sensors = forms.ModelMultipleChoiceField(
queryset=Sensor.objects.all(),
required=False,
widget=forms.CheckboxSelectMultiple,
label='Sensors'
)
class Meta:
model = Device
fields = ['name', 'ip', 'protocol', 'temperature', 'humidity']
fields = ['name', 'ip', 'protocol']
def __init__(self, *args, **kwargs):
# Optionally pass initial sensors for editing an existing device
if 'instance' in kwargs:
initial_sensors = kwargs['instance'].sensors.all() if kwargs['instance'] else None
initial = kwargs.get('initial', {})
initial['sensors'] = initial_sensors
kwargs['initial'] = initial
super(DeviceForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
# Save the device instance
device = super(DeviceForm, self).save(commit=False)
if commit:
device.save()
self.save_m2m() # Ensure M2M save happens
return device
class SensorWithTypeForm(forms.ModelForm):
# Add fields for SensorType directly in the form
type_name = forms.CharField(max_length=50, label="Sensor Type Name")
unit = forms.CharField(max_length=20, label="Unit", required=False)
protocol = forms.ChoiceField(
choices=[('mqtt', 'MQTT'), ('http', 'HTTP')],
label="Protocol"
)
topic = forms.CharField(max_length=100, label="Topic", required=False)
endpoint = forms.CharField(max_length=100, label="Endpoint", required=False)
class Meta:
model = Sensor
fields = ['device', 'enabled']
def save(self, commit=True):
# Create or get the SensorType
try:
sensor_type = SensorType.objects.get(name=self.cleaned_data['type_name'])
except SensorType.DoesNotExist:
sensor_type = SensorType(
name=self.cleaned_data['type_name'],
unit=self.cleaned_data['unit'],
protocol=self.cleaned_data['protocol'],
topic=self.cleaned_data['topic'],
endpoint=self.cleaned_data['endpoint']
)
if commit:
sensor_type.save()
# Create Sensor with the SensorType found or created
sensor = super(SensorWithTypeForm, self).save(commit=False)
sensor.type = sensor_type
if commit:
sensor.save()
return sensor

41
iotDashboard/gpt.py Normal file
View File

@@ -0,0 +1,41 @@
import json
import redis
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI()
redis_client = redis.StrictRedis(host='10.10.0.1', port=6379, db=0)
data = redis_client.get("last5").decode("utf-8")
def analysis(environment_data):
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system",
"content": "You are an assistant that analyzes environmental data for an office working space and provides "
"concise numerical insights."},
{
"role": "user",
"content": f"Analyze the following environmental data. The goal is maintaining optimal working "
f"conditions in the office and peak working brain. Focus on any outliers or necessary adjustments. The data is as following: {environment_data}."
f"The output should be only the recommendations in numerical form with postitive and negative "
f"numbers and also provide small summary in a sentence or two of the current conditions and "
f"easily computable in json format. Be consistent with the + and - signs and the summary"
}
],
response_format={"type": "json_object"}
)
output = completion.choices[0].message.content
return output
output = analysis(data)
redis_client.set("gpt",json.dumps(output))
print(output)

View File

@@ -0,0 +1,47 @@
# Generated by Django 4.2.5 on 2024-10-08 10:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('iotDashboard', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SensorType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('unit', models.CharField(max_length=20)),
('protocol', models.CharField(choices=[('mqtt', 'MQTT'), ('http', 'HTTP')], max_length=20)),
('topic', models.CharField(blank=True, max_length=100, null=True)),
('endpoint', models.CharField(blank=True, max_length=100, null=True)),
],
),
migrations.RemoveField(
model_name='device',
name='humidity',
),
migrations.RemoveField(
model_name='device',
name='temperature',
),
migrations.AlterField(
model_name='device',
name='protocol',
field=models.CharField(choices=[('mqtt', 'MQTT'), ('http', 'HTTP')], max_length=20),
),
migrations.CreateModel(
name='Sensor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('enabled', models.BooleanField(default=True)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sensors', to='iotDashboard.device')),
('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iotDashboard.sensortype')),
],
),
]

View File

@@ -1,10 +1,26 @@
from django.db import models
class Device(models.Model):
name = models.CharField(max_length=50)
ip = models.CharField(max_length=20)
protocol = models.CharField(max_length=20)
temperature = models.BooleanField(default=False)
humidity = models.BooleanField(default=False)
class SensorType(models.Model):
name = models.CharField(max_length=50, unique=True) # Sensor name, e.g., "CO2", "Noise", etc.
unit = models.CharField(max_length=20) # Unit of measurement, e.g., "ppm", "dB", "lux"
protocol = models.CharField(max_length=20, choices=[('mqtt', 'MQTT'), ('http', 'HTTP')]) # Protocol for communication
topic = models.CharField(max_length=100, null=True, blank=True) # Topic for MQTT communication
endpoint = models.CharField(max_length=100, null=True, blank=True) # Endpoint for HTTP communication
def __str__(self):
return self.name
return f"{self.name} ({self.unit})"
class Device(models.Model):
name = models.CharField(max_length=50) # Device name
ip = models.CharField(max_length=20) # Device IP address
protocol = models.CharField(max_length=20, choices=[('mqtt', 'MQTT'), ('http', 'HTTP')])
def __str__(self):
return self.name
class Sensor(models.Model):
device = models.ForeignKey(Device, related_name='sensors', on_delete=models.CASCADE)
type = models.ForeignKey(SensorType, on_delete=models.CASCADE)
enabled = models.BooleanField(default=True)
def __str__(self):
return f"{self.type.name} Sensor on {self.device.name}"

View File

@@ -6,83 +6,104 @@ import paho.mqtt.client as mqtt
import redis
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Set up Redis client
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
redis_client = redis.StrictRedis(host=REDIS_HOST, port=6379, db=0)
print("Connected to Redis Server")
# MQTT broker address
MQTT_BROKER = os.getenv('MQTT_BROKER')
mqtt_data = {}
def get_devices():
"""" Simple method to get all devices """
# Get devices from Redis
devices_json = redis_client.get('devices')
def get_mqtt_devices():
"""Retrieve MQTT devices and sensor details from Redis."""
devices_json = redis_client.get('mqtt_devices')
if devices_json:
return json.loads(devices_json)
return []
def build_device_map():
"""Build a mapping of device endpoints to friendly names."""
devices = get_mqtt_devices()
return {device['topic'].split('/')[0]: device['device_name'] for device in
devices} # Assuming topic starts with device name
def on_message(client, userdata, msg):
"""" Simple method to handle messages """
topic = msg.topic.split('/')
device_name = topic[0]
sensor = topic[-2]
"""Handle incoming messages from MQTT broker."""
try:
# Parse the incoming message topic
topic_parts = msg.topic.split('/')
device_endpoint = topic_parts[0] # This is the actual endpoint name
sensor_type = topic_parts[2] # Assuming sensor type is in the third part
if device_name not in mqtt_data:
mqtt_data[device_name] = {"time": datetime.now(),
"device": device_name,
"temperature": None,
"humidity": None}
sensor_value = float(msg.payload.decode())
print(f"Received message from {device_endpoint}, sensor {sensor_type}: {sensor_value}")
if sensor == "tempreature":
mqtt_data[device_name]["temperature"] = float(msg.payload.decode())
elif sensor == "humidity":
mqtt_data[device_name]["humidity"] = float(msg.payload.decode())
# Build the device map to get the friendly device name
device_map = build_device_map()
device_name = device_map.get(device_endpoint, device_endpoint) # Fallback to endpoint if not found
if device_name not in mqtt_data:
mqtt_data[device_name] = {
"time": datetime.utcnow().isoformat(),
"device": device_name,
"sensors": {}
}
# Update the sensor value in the mqtt_data dictionary
mqtt_data[device_name]["sensors"][sensor_type] = sensor_value
mqtt_data[device_name]["time"] = datetime.utcnow().isoformat()
# Store the updated data structure in Redis
redis_client.set(device_name, json.dumps(mqtt_data[device_name]))
print(f"Updated data for {device_name}: {mqtt_data[device_name]}")
except ValueError as e:
print(f"Error processing message payload: {e}")
mqtt_data[device_name]["time"] = str(datetime.now())
redis_client.set(device_name, json.dumps(mqtt_data))
print(f"Updated data for {device_name}: {mqtt_data[device_name]}")
def on_connect(client, userdata, flags, rc):
"""Handle successful connection."""
print(f"Connected with result code {rc}")
devices = get_devices()
for device in devices:
client.subscribe(f"{device['name']}/sensor/+/state")
"""Handle successful MQTT connection."""
if rc == 0:
print("Connected to MQTT Broker")
devices = get_mqtt_devices()
for device in devices:
client.subscribe(device['topic']) # Subscribing to each device's topic
print(f"Subscribed to topic: {device['topic']}")
else:
print(f"Failed to connect, return code {rc}")
def on_disconnect(client, userdata, rc):
"""Handle disconnection."""
if rc != 0:
print(f"Unexpected disconnection. Result code: {rc}")
"""Handle disconnection from MQTT broker."""
print(f"Disconnected with result code: {rc}")
def start_mqtt_client():
""" Start the MQTT client """
devices = get_devices()
"""Start the MQTT client to begin listening to topics."""
client = mqtt.Client()
client.on_message = on_message
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
client.connect(MQTT_BROKER)
client.loop_start()
print("MQTT Client Started")
for device in devices:
client.subscribe(f"{device['name']}/sensor/+/state")
# Keep the script running
try:
while True:
time.sleep(10) # Sleep to prevent high CPU usage
except KeyboardInterrupt:
print("Script interrupted by user")
finally:
client.loop_stop() # Stop the loop when exiting
client.loop_stop()
if __name__ == "__main__":
start_mqtt_client()
start_mqtt_client()

View File

@@ -2,91 +2,154 @@ import json
import datetime
import requests
import psycopg2
import redis
from django.conf import settings
from huey import crontab
from huey.contrib.djhuey import periodic_task
from .models import Device
import redis
from .models import Device, Sensor, SensorType
# Initialize Redis client
redis_client = redis.StrictRedis(host='10.10.0.1', port=6379, db=0)
def devices_to_redis():
"""Fetch all devices from Django and store them in Redis."""
"""Fetch devices and their sensors' topics from Django and store them in Redis."""
devices = Device.objects.all()
devices_list = [
{
'id': device.id,
'name': device.name,
'protocol': device.protocol,
'ip': device.ip,
}
for device in devices
]
redis_client.set('devices', json.dumps(devices_list))
devices_list = []
for device in devices:
for sensor in device.sensors.all():
sensor_data = {
'device_name': device.name,
'sensor_name': sensor.type.name,
'topic': sensor.type.topic # Assuming the topic is stored in SensorType
}
devices_list.append(sensor_data)
redis_client.set('mqtt_devices', json.dumps(devices_list))
print("Devices with sensors stored in Redis.")
def fetch_data_http(device):
"""Fetch temperature and humidity data from an HTTP sensor."""
data = {
"time": datetime.datetime.now().isoformat(),
"device": device.name,
}
def fetch_data_http(device, sensor):
"""Fetch data from an HTTP sensor."""
sensor_type_name = sensor.type.name.lower()
try:
temperature_response = requests.get(f"http://{device.ip}/sensor/tempreature")
humidity_response = requests.get(f"http://{device.ip}/sensor/humidity")
data["temperature"] = temperature_response.json().get('value')
data["humidity"] = humidity_response.json().get('value')
# Make the request to the device's HTTP endpoint
response = requests.get(f"http://{device.ip}/sensor/{sensor_type_name}", timeout=5)
response.raise_for_status() # Raise an exception for any non-200 status codes
sensor_value = response.json().get('value') # Assuming the JSON response structure
if sensor_value is not None:
return {
"time": datetime.datetime.utcnow().isoformat(),
"device": device.name,
"sensor": sensor_type_name,
"sensor_value": sensor_value
}
else:
print(f"No value returned from {device.name} for {sensor_type_name}")
except requests.RequestException as e:
print(f"HTTP request failed: {e}")
return data
print(f"HTTP request failed for {device.name}: {e}")
return None
def fetch_data_mqtt(device_name):
"""Fetch data from Redis for a specific MQTT device."""
data = redis_client.get(device_name)
def fetch_data_mqtt(device, sensor):
"""Fetch data from Redis for a specific MQTT device and sensor."""
# Get the data for the specific device from Redis
data = redis_client.get(device.name) # Assumes device.name is the Redis key
if data:
data = json.loads(data.decode('utf-8')).get(device_name)
if data and datetime.datetime.fromisoformat(data["time"]) > datetime.datetime.now() - datetime.timedelta(
minutes=2):
return data
data = json.loads(data.decode('utf-8'))
# Normalize the sensor name to lowercase for lookup
sensor_name = sensor.type.name.lower()
sensor_value = data['sensors'].get(sensor_name)
if sensor_value is not None and is_recent_data(data['time']):
return {
"time": data['time'],
"device": device.name,
"sensor_value": sensor_value
}
print(data)
return None
def insert_data(data):
"""Insert data into the PostgreSQL database."""
with psycopg2.connect(settings.CONNECTION_STRING) as conn:
with conn.cursor() as cursor:
insert_query = """
INSERT INTO conditions (time, device, temperature, humidity)
VALUES (%s, %s, %s, %s)
"""
cursor.execute(insert_query, (data["time"], data["device"], data["temperature"], data["humidity"]))
conn.commit()
def is_recent_data(timestamp):
"""Check if data is within a 2-minute freshness window."""
data_time = datetime.datetime.fromisoformat(timestamp)
return data_time > datetime.datetime.utcnow() - datetime.timedelta(minutes=2)
def insert_data(data, sensor_type):
"""Insert parsed data into the PostgreSQL database."""
if 'sensor_value' not in data:
print(f"Missing 'sensor_value' in data: {data}. Skipping insertion.")
return
insert_data_dict = {
"time": data['time'],
"device": data['device'],
"metric": sensor_type.lower(),
"value": data['sensor_value'],
}
try:
with psycopg2.connect(settings.CONNECTION_STRING) as conn:
with conn.cursor() as cursor:
insert_query = """
INSERT INTO sensor_readings (time, device_name, metric, value)
VALUES (%s, %s, %s, %s);
"""
cursor.execute(insert_query, (
insert_data_dict["time"],
insert_data_dict["device"],
insert_data_dict["metric"],
insert_data_dict["value"]
))
conn.commit()
print(f"Data inserted successfully for {insert_data_dict['device']}: {insert_data_dict}")
except Exception as e:
print(f"Failed to insert data: {e}")
@periodic_task(crontab(minute='*/1'))
def fetch_data_from_all_devices():
"""Fetch and insert data for all devices based on their protocol."""
devices = Device.objects.all()
for device in devices:
data = None
if device.protocol == 'http':
data = fetch_data_http(device)
elif device.protocol == 'mqtt':
data = fetch_data_mqtt(device.name)
for sensor in device.sensors.all():
data = None
if data:
data_time = datetime.datetime.fromisoformat(data["time"])
if data_time > datetime.datetime.now() - datetime.timedelta(minutes=1):
insert_data(data)
if device.protocol == 'http':
data = fetch_data_http(device, sensor)
elif device.protocol == 'mqtt':
data = fetch_data_mqtt(device, sensor)
if data and is_recent_data(data['time']):
insert_data(data, sensor.type.name)
else:
print(f"No recent data available for {device.name}. Skipping insertion.")
else:
print(f"No data available for {device.name}. Skipping insertion.")
print(f"No recent or valid data for {device.name}. Skipping.")
@periodic_task(crontab(minute='*/5'))
def last_5_minutes():
"""Fetch the last 5 readings from TimescaleDB and store them in Redis."""
try:
with psycopg2.connect(settings.CONNECTION_STRING) as conn:
with conn.cursor() as cursor:
cursor.execute("""
SELECT time, device_name, metric, value
FROM sensor_readings
ORDER BY time DESC
LIMIT 5;
""")
results = cursor.fetchall()
data = [
{
"time": reading[0].isoformat(),
"device": reading[1],
"metric": reading[2],
"value": reading[3]
}
for reading in results
]
redis_client.set("last5", json.dumps(data))
print("Last 5 readings:", data)
except Exception as e:
print(f"Error fetching or storing the last 5 readings: {e}")
# Initialize device data in Redis
devices_to_redis()

View File

@@ -1,222 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Conditions Chart with Chart.js</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<title>IoT Sensor Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.chart-container {
position: relative;
width: 100%;
height: 400px;
background-color: #f9f9f9;
padding: 20px;
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1);
}
#conditionsChart {
width: 100% !important;
height: 100% !important;
}
.current-conditions {
text-align: center;
padding: 20px;
.device-panel {
margin-bottom: 20px;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1);
}
.current-conditions h2 {
font-size: 36px;
margin: 10px 0;
}
.current-conditions .value {
font-size: 48px;
font-weight: bold;
margin-bottom: 10px;
}
</style>
</head>
<body class="container my-4">
<!-- GPT Summary -->
<h4 class="text-center mb-4">{{ gpt }}</h4>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'chart' %}">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'device_list' %}">Devices</a>
</li>
</ul>
<ul class="navbar-nav">
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<h1 class="text-center mb-4">IoT Sensor Data Dashboard</h1>
<div class="container mt-5">
<h1 class="text-center mb-4">Temperature and Humidity Over Time</h1>
<!-- Button to add a new "island" (device panel) -->
<div class="text-center mb-4">
<button class="btn btn-success" onclick="addDevicePanel()">Add Device Panel</button>
</div>
<!-- Current Conditions -->
<div class="current-conditions">
<h2>Current Conditions</h2>
<div class="value" id="current-temperature">Loading...</div>
<div class="value" id="current-humidity">Loading...</div>
</div>
<!-- Device Selector Dropdown -->
<div class="row mb-4">
<div class="col-md-4 offset-md-4">
<select id="deviceSelector" class="form-select" onchange="fetchDeviceData()">
{% for device in devices %}
<option value="{{ device.name }}">{{ device.name }} ({{ device.ip }})</option>
{% endfor %}
</select>
</div>
</div>
<!-- Date Pickers for Time Window -->
<div class="row mb-4">
<div class="col-md-6 offset-md-3 d-flex justify-content-between">
<div>
<label for="startDate" class="form-label">Start Date:</label>
<input type="datetime-local" id="startDate" class="form-control" onchange="fetchDeviceData()">
</div>
<div>
<label for="endDate" class="form-label">End Date:</label>
<input type="datetime-local" id="endDate" class="form-control" onchange="fetchDeviceData()">
</div>
</div>
</div>
<!-- Chart Container -->
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="chart-container">
<canvas id="conditionsChart"></canvas>
</div>
</div>
</div>
<!-- Container for dynamic device panels ("islands") -->
<div id="device-panels" class="row">
<!-- Dynamic panels will be added here -->
</div>
<script>
var ctx = document.getElementById('conditionsChart').getContext('2d');
var conditionsChart;
const devices = {{ devices_json|safe }};
let panelCount = 0;
const charts = {};
function fetchDeviceData() {
var device = document.getElementById('deviceSelector').value;
var startDate = document.getElementById('startDate').value;
var endDate = document.getElementById('endDate').value;
// Function to add a new device panel
function addDevicePanel() {
const panelId = `device-panel-${panelCount}`;
const chartId = `sensorChart-${panelCount}`;
const panelHtml = `
<div class="col-md-6 device-panel" id="${panelId}">
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>Device Panel</span>
<button class="btn btn-danger btn-sm" onclick="removeDevicePanel('${panelId}')">Remove</button>
</div>
<div class="card-body">
<div class="mb-3">
<label for="device-select-${panelId}" class="form-label">Choose a device:</label>
<select id="device-select-${panelId}" class="form-select" onchange="fetchSensorData('${panelId}', '${chartId}')">
<!-- Dynamically populated options -->
</select>
</div>
fetch(`/fetch_device_data/?device=${device}&start_date=${startDate}&end_date=${endDate}`)
<!-- Date and Time Selection -->
<div class="mb-3">
<label for="start-date-${panelId}" class="form-label">Start Date and Time:</label>
<input type="datetime-local" id="start-date-${panelId}" class="form-control">
</div>
<div class="mb-3">
<label for="end-date-${panelId}" class="form-label">End Date and Time:</label>
<input type="datetime-local" id="end-date-${panelId}" class="form-control">
</div>
<button class="btn btn-primary w-100" onclick="fetchSensorData('${panelId}', '${chartId}')">Fetch Data</button>
<canvas id="${chartId}" class="mt-4"></canvas>
</div>
</div>
</div>`;
document.getElementById('device-panels').insertAdjacentHTML('beforeend', panelHtml);
populateDeviceSelect(`device-select-${panelId}`);
const ctx = document.getElementById(chartId).getContext('2d');
charts[chartId] = new Chart(ctx, {
type: 'line',
data: { labels: [], datasets: [] },
options: { responsive: true }
});
panelCount++; // Increment panelCount after using it
}
// Function to populate device dropdown
function populateDeviceSelect(selectId) {
const deviceSelect = document.getElementById(selectId);
deviceSelect.innerHTML = ""; // Clear previous options
devices.forEach(device => {
const option = document.createElement('option');
option.value = device.name;
option.text = `${device.name} (${device.sensors__type__name})`;
deviceSelect.appendChild(option);
});
}
// Function to remove a device panel
function removeDevicePanel(panelId) {
const panel = document.getElementById(panelId);
panel.remove();
}
// Function to fetch sensor data and update the chart for a specific panel
function fetchSensorData(panelId, chartId) {
const device = document.getElementById(`device-select-${panelId}`).value;
const startDate = document.getElementById(`start-date-${panelId}`).value;
const endDate = document.getElementById(`end-date-${panelId}`).value;
if (!device || !startDate || !endDate) {
alert('Please select a device and both start and end date/time.');
return;
}
fetch(`/fetch_device_data?device=${device}&start_date=${startDate}&end_date=${endDate}`)
.then(response => response.json())
.then(data => {
if (conditionsChart) {
conditionsChart.destroy(); // Destroy the old chart
}
const times = Object.values(data)[0].times;
const datasets = Object.keys(data).map(metric => ({
label: metric,
data: data[metric].values,
borderColor: getRandomColor(),
fill: false
}));
conditionsChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.times,
datasets: [
{
label: 'Temperature (°C)',
data: data.temperatures,
borderColor: 'red',
fill: false,
},
{
label: 'Humidity (%)',
data: data.humidities,
borderColor: 'blue',
fill: false,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
title: {
display: true,
text: 'Time'
},
ticks: {
autoSkip: true,
maxRotation: 45,
minRotation: 45,
}
},
y: {
title: {
display: true,
text: 'Values'
},
beginAtZero: true
}
},
plugins: {
legend: {
display: true,
position: 'top',
labels: {
boxWidth: 20,
padding: 20,
}
},
tooltip: {
enabled: true,
}
}
}
});
// Check if the last recorded data is within the last 10 minutes
const lastRecordedTime = new Date(data.times[data.times.length - 1]);
const now = new Date();
const tenMinutesAgo = new Date(now.getTime() - 10 * 60000);
if (lastRecordedTime > tenMinutesAgo) {
document.getElementById('current-temperature').textContent = `Temperature: ${data.temperatures[data.temperatures.length - 1]}°C`;
document.getElementById('current-humidity').textContent = `Humidity: ${data.humidities[data.humidities.length - 1]}%`;
} else {
document.getElementById('current-temperature').textContent = `Temperature: -`;
document.getElementById('current-humidity').textContent = `Humidity: -`;
}
charts[chartId].data.labels = times;
charts[chartId].data.datasets = datasets;
charts[chartId].update();
});
}
// Initial load for the default device and time range
fetchDeviceData();
// Function to generate random color for chart lines
function getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) color += letters[Math.floor(Math.random() * 16)];
return color;
}
// Initialize with one device panel
addDevicePanel();
</script>
<!-- Bootstrap JS and dependencies (Optional) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -5,9 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if form.instance.pk %}Edit{% else %}Add{% endif %} Device</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
@@ -16,7 +18,7 @@
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'chart' %}">Chart</a>
<a class="nav-link" href="{% url 'index' %}">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'device_list' %}">Devices</a>
@@ -36,14 +38,29 @@
</div>
</div>
</nav>
<div class="container mt-5">
<h1 class="text-center mb-4">{% if form.instance.pk %}Edit{% else %}Add{% endif %} Device</h1>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<!-- Display the form fields -->
{{ form.as_p }}
<!-- If there are errors, display them -->
{% if form.errors %}
<div class="alert alert-danger">
<ul>
{% for field, errors in form.errors.items %}
<li>{{ field }}: {{ errors|join:", " }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<!-- Submit and Cancel buttons -->
<button type="submit" class="btn btn-success">Save</button>
<a href="{% url 'device_list' %}" class="btn btn-secondary">Cancel</a>
</form>

View File

@@ -7,7 +7,8 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
@@ -15,30 +16,23 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'chart' %}">Chart</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'device_list' %}">Devices</a>
</li>
<li class="nav-item"><a class="nav-link" href="{% url 'index' %}">Chart</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul>
<ul class="navbar-nav">
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
</li>
<li class="nav-item"><a class="nav-link" href="{% url 'logout' %}">Logout</a></li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Login</a>
</li>
<li class="nav-item"><a class="nav-link" href="{% url 'login' %}">Login</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Device Management -->
<div class="container mt-5">
<h1 class="text-center mb-4">Manage Devices</h1>
<a href="{% url 'add_device' %}" class="btn btn-primary mb-3">Add Device</a>
<table class="table table-striped">
@@ -47,8 +41,7 @@
<th>Name</th>
<th>IP Address</th>
<th>Protocol</th>
<th>Temperature Monitoring</th>
<th>Humidity Monitoring</th>
<th>Sensor Types</th> <!-- Adjusted to Sensor Types -->
<th>Actions</th>
</tr>
</thead>
@@ -58,16 +51,27 @@
<td>{{ device.name }}</td>
<td>{{ device.ip }}</td>
<td>{{ device.protocol }}</td>
<td>{{ device.temperature|yesno:"Yes,No" }}</td>
<td>{{ device.humidity|yesno:"Yes,No" }}</td>
<!-- Collect and display sensor types -->
<td>
{% for sensor in device.sensors.all %}
{{ sensor.type.name }}{% if not forloop.last %}, {% endif %}
{% empty %}
No sensors
{% endfor %}
</td>
<!-- You may further expand other sensor-related data -->
<td>
<a href="{% url 'edit_device' device.pk %}" class="btn btn-warning btn-sm">Edit</a>
<a href="{% url 'delete_device' device.pk %}" class="btn btn-danger btn-sm">Delete</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center">No devices found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
</html>

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Add Sensor and Type</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">IoT Dashboard</a>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{% url 'device_list' %}">Devices</a></li>
</ul>
</div>
</div>
</nav>
<div class="container mt-5">
<h1 class="text-center mb-4">Add Sensor and Sensor Type</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }} <!-- Renders the inputs for all fields you added in your form -->
<button type="submit" class="btn btn-success">Save</button>
<a href="{% url 'device_list' %}" class="btn btn-secondary">Cancel</a>
</form>
{% if form.errors %}
<div class="alert alert-danger">
<ul>
{% for field, errors in form.errors.items %}
<li>{{ field }}: {{ errors|join:', ' }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</body>
</html>

View File

@@ -20,12 +20,13 @@ from iotDashboard import views
urlpatterns = [
path('admin/', admin.site.urls),
path('',views.index),
path('devices_api/',views.devices_api),
path('',views.chart,name="index"),
path('fetch_device_data/', views.fetch_device_data, name='fetch_device_data'),
path('chart/',views.chart,name='chart'),
path('devices/', views.device_list, name='device_list'),
path('devices/add/', views.add_device, name='add_device'),
path('devices/edit/<int:pk>/', views.edit_device, name='edit_device'),
path('devices/delete/<int:pk>/', views.delete_device, name='delete_device'),
path('logout/', views.logout_view, name='logout'),
path('sensor/add/',views.add_sensor_with_type,name="add_sensor_with_type")
]

View File

@@ -1,57 +1,105 @@
from django.http import HttpResponse, request, JsonResponse
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.http import JsonResponse, HttpResponse
from django.db import connections
from django.shortcuts import render, redirect, get_object_or_404
from .models import Device
from .forms import DeviceForm
from django.views.decorators.http import require_GET
from .models import Device, Sensor, SensorType
from .forms import DeviceForm, SensorWithTypeForm
import redis
redis_client = redis.StrictRedis(host='10.10.0.1', port=6379, db=0)
def fetch_gpt_data():
return redis_client.get("gpt").decode("utf-8").strip('b"').replace('\\"', '"').replace("\\n", "").replace("\\", "")
def chart(request):
# Fetch devices and their related sensors
devices = list(Device.objects.all().values('name', 'sensors__type__name'))
# Serialize data to JSON format
devices_json = json.dumps(devices, cls=DjangoJSONEncoder)
# Pass devices data to the context
gpt = fetch_gpt_data()
gpt = json.loads(gpt)
context = {'devices_json': devices_json, 'gpt': gpt["summary"]}
return render(request, 'chart.html', context)
# Fetch sensor data (AJAX)
def fetch_device_data(request):
device = request.GET.get('device', 'livingroom')
device_name = request.GET.get('device', 'Livingroom')
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
query = """
SELECT time, temperature, humidity
FROM conditions
WHERE device = %s
"""
params = [device]
# Log the parameters to ensure they are correct
print("Device Name:", device_name)
print("Start Date:", start_date)
print("End Date:", end_date)
# Get the specific device by name
device = get_object_or_404(Device, name=device_name)
# Initialize the results dictionary to store sensor data
results = {}
# Prepare SQL query and parameters for the specific sensor type
query = """
SELECT time, metric, value
FROM sensor_readings
WHERE device_name = %s
"""
params = [device.name]
# Add time filtering to the query
if start_date:
query += " AND time >= %s"
query += " AND time >= %s::timestamptz"
params.append(start_date)
if end_date:
query += " AND time <= %s"
query += " AND time <= %s::timestamptz"
params.append(end_date)
# Log the final query and params
print("Final Query:", query)
print("Params Before Execution:", params)
# Fetch data from the database
with connections["data"].cursor() as cursor:
cursor.execute(query, params)
rows = cursor.fetchall()
times = [row[0].strftime('%Y-%m-%d %H:%M:%S') for row in rows]
temperatures = [row[1] for row in rows]
humidities = [row[2] for row in rows]
# Process the results and group them by sensor type (metric)
for row in rows:
time, metric, value = row
formatted_time = time.strftime('%Y-%m-%d %H:%M:%S')
return JsonResponse({
'times': times,
'temperatures': temperatures,
'humidities': humidities,
})
def chart(request):
devices = Device.objects.all()
context = {'devices': devices}
return render(request, 'chart.html', context)
if metric not in results:
results[metric] = {
'times': [],
'values': []
}
results[metric]['times'].append(formatted_time)
results[metric]['values'].append(value)
return JsonResponse(results)
def index(request):
if request.user.is_authenticated:
return redirect("/chart/")
return HttpResponse("NOT AUTHENTICATED!!!")
def device_list(request):
devices = Device.objects.all()
return render(request, 'device_list.html', {'devices': devices})
def add_device(request):
if request.method == 'POST':
form = DeviceForm(request.POST)
@@ -62,6 +110,7 @@ def add_device(request):
form = DeviceForm()
return render(request, 'device_form.html', {'form': form})
def edit_device(request, pk):
device = get_object_or_404(Device, pk=pk)
if request.method == 'POST':
@@ -73,6 +122,7 @@ def edit_device(request, pk):
form = DeviceForm(instance=device)
return render(request, 'device_form.html', {'form': form})
def delete_device(request, pk):
device = get_object_or_404(Device, pk=pk)
if request.method == 'POST':
@@ -80,5 +130,22 @@ def delete_device(request, pk):
return redirect('device_list')
return render(request, 'device_confirm_delete.html', {'device': device})
def logout_view():
redirect("/admin")
def add_sensor_with_type(request):
if request.method == 'POST':
form = SensorWithTypeForm(request.POST)
if form.is_valid():
form.save() # This will save both Sensor and SensorType as needed
return redirect('device_list') # Adjust this to your specific URL name
else:
form = SensorWithTypeForm()
context = {'form': form}
return render(request, 'sensor_form.html', context)
def logout_view(request):
return redirect("/admin")
def devices_api(request):
devices = list(Device.objects.all().values('name', 'sensors__type__name'))
return JsonResponse(devices, safe=False)