Skip to content

Commit a992f8a

Browse files
authored
Merge pull request #3124 from FoamyGuy/sensor_llm_project
Sensor data LLM project code
2 parents c1fe7c2 + 7e179f6 commit a992f8a

File tree

5 files changed

+400
-0
lines changed

5 files changed

+400
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
from datetime import datetime
5+
from sqlalchemy import Column, Integer, Float, String, DateTime, create_engine
6+
from sqlalchemy.ext.declarative import declarative_base
7+
8+
9+
Base = declarative_base()
10+
11+
12+
class SensorReading(Base):
13+
"""
14+
Database model for environmental sensor readings.
15+
"""
16+
17+
__tablename__ = "sensor_readings"
18+
19+
id = Column(Integer, primary_key=True, autoincrement=True)
20+
datetime = Column(DateTime, nullable=False, default=datetime.utcnow)
21+
room_name = Column(String(100), nullable=False)
22+
temperature_c = Column(Float, nullable=True)
23+
temperature_f = Column(Float, nullable=True)
24+
humidity = Column(Float, nullable=True) # Percentage
25+
pm25 = Column(Float, nullable=True) # PM2.5 in µg/m³
26+
voc_index = Column(Float, nullable=True) # VOC index
27+
nox_index = Column(Float, nullable=True) # NOx index
28+
co2 = Column(Float, nullable=True) # CO2 in ppm
29+
30+
def __repr__(self):
31+
return (
32+
f"<SensorReading(room='{self.room_name}', "
33+
f"datetime='{self.datetime}', "
34+
f"temp_c={self.temperature_c}, "
35+
f"humidity={self.humidity})>"
36+
)
37+
38+
39+
if __name__ == "__main__":
40+
41+
def create_database(db_url="sqlite:///sensor_data.db"):
42+
"""Create the database and all tables."""
43+
engine = create_engine(db_url, echo=True)
44+
45+
# Create all tables
46+
Base.metadata.create_all(engine)
47+
48+
print(f"Database created successfully at: {db_url}")
49+
return engine
50+
51+
# Create the database when script is run directly
52+
create_database()
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import csv
5+
from datetime import datetime, timedelta, UTC
6+
from sqlalchemy import create_engine
7+
from sqlalchemy.orm import sessionmaker
8+
from ollama import chat
9+
from db_models import SensorReading
10+
11+
# pylint: disable=too-many-locals, too-many-nested-blocks
12+
13+
# Database connection configuration
14+
DATABASE_URL = "sqlite:///sensor_data.db"
15+
16+
MODEL = "gemma3:1b"
17+
18+
# Room name to pull data for. Update to match one of your rooms.
19+
ROOM = "Basement"
20+
21+
# Specify a Custom Date Range
22+
# QUERY_START = datetime(2025, 9, 16, 0, 0, 0, tzinfo=UTC)
23+
# QUERY_END = datetime(2025, 9, 18, 19, 0, 0, tzinfo=UTC)
24+
25+
# Defaults to last 24 hours if start and end are None
26+
QUERY_START = None
27+
QUERY_END = None
28+
29+
# Time interval in minutes to export data with i.e. one data point every 30 minutes.
30+
SAMPLE_RATE = 30 # minutes
31+
32+
33+
PROMPT = """Analyze the following environmental sensor data. Provide a summary of its content,
34+
identify key patterns or insights, and suggest potential further analysis or questions based on this data.
35+
36+
Data:
37+
---
38+
%%_DATA_PLACEHOLDER_%%
39+
---
40+
41+
The data fields are:
42+
- UTC Datetime
43+
- Temperature in degrees F
44+
- Humidity percent
45+
- pm2.5 in µg/m³
46+
- VOC index
47+
- NOx index
48+
- CO2 in ppm
49+
50+
Please summarize the data, identify key patterns, insights, or trends.
51+
"""
52+
53+
54+
def fetch_data(
55+
room, start_datetime=None, end_datetime=None, output_file=None, sample_rate=30
56+
):
57+
"""
58+
Fetch all SensorReading records from a specified time range for RoomC
59+
and save them to a CSV file
60+
61+
Args:
62+
room (str): Room name
63+
start_datetime (Optional[datetime]): Start of the time range (default: 24 hours ago)
64+
end_datetime (Optional[datetime]): End of the time range (default: now)
65+
output_file (str): Name of the CSV file to create
66+
sample_rate (int): Sampling interval in minutes (e.g., 5 for every 5 minutes)
67+
"""
68+
# Create database engine and session
69+
engine = create_engine(DATABASE_URL)
70+
Session = sessionmaker(bind=engine)
71+
session = Session()
72+
73+
try:
74+
# Set default values if not provided
75+
if end_datetime is None:
76+
end_datetime = datetime.now(UTC)
77+
if start_datetime is None:
78+
start_datetime = end_datetime - timedelta(hours=24)
79+
80+
# Ensure start_datetime is before end_datetime
81+
if start_datetime >= end_datetime:
82+
raise ValueError("start_datetime must be before end_datetime")
83+
84+
print(f"Fetching data for {room} from {start_datetime} to {end_datetime}")
85+
86+
# Query for RoomC records within the specified time range
87+
query = (
88+
session.query(SensorReading)
89+
.filter(
90+
SensorReading.room_name == room,
91+
SensorReading.datetime >= start_datetime,
92+
SensorReading.datetime <= end_datetime,
93+
)
94+
.order_by(SensorReading.datetime.desc())
95+
)
96+
97+
# Execute the query
98+
results = query.all()
99+
100+
# Apply sampling if sample_rate > 1
101+
if sample_rate > 1:
102+
sampled_results = []
103+
if results:
104+
# Start from the most recent record (first in desc order)
105+
base_time = results[0].datetime
106+
107+
for reading in results:
108+
# Calculate minutes difference from the base time
109+
time_diff = abs((base_time - reading.datetime).total_seconds() / 60)
110+
111+
# Include reading if it falls on a sample interval
112+
if time_diff % sample_rate < 1: # Allow 1 minute tolerance
113+
sampled_results.append(reading)
114+
115+
results = sampled_results
116+
print(
117+
f"Applied {sample_rate}-minute sampling: {len(results)} records selected"
118+
)
119+
120+
if output_file is not None:
121+
# Write results to CSV file
122+
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
123+
fieldnames = [
124+
"datetime",
125+
"temperature_f",
126+
"humidity",
127+
"pm25",
128+
"voc_index",
129+
"nox_index",
130+
"co2",
131+
]
132+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
133+
134+
# Write header
135+
writer.writeheader()
136+
137+
# Write data rows
138+
for reading in results:
139+
writer.writerow(
140+
{
141+
"datetime": (
142+
reading.datetime.isoformat()
143+
if reading.datetime
144+
else None
145+
),
146+
"temperature_f": reading.temperature_f,
147+
"humidity": reading.humidity,
148+
"pm25": reading.pm25,
149+
"voc_index": reading.voc_index,
150+
"nox_index": reading.nox_index,
151+
"co2": reading.co2,
152+
}
153+
)
154+
155+
time_range = end_datetime - start_datetime
156+
print(
157+
f"Successfully saved {len(results)} records for RoomC to '{output_file}'"
158+
+ f" (time range: {time_range})"
159+
+ (f" (sampled every {sample_rate} minutes)" if sample_rate > 1 else "")
160+
)
161+
return results
162+
# pylint:disable=broad-except
163+
except Exception as e:
164+
print(f"Error fetching data: {e}")
165+
return []
166+
167+
finally:
168+
# Always close the session
169+
session.close()
170+
171+
172+
if __name__ == "__main__":
173+
174+
records = fetch_data(
175+
room=ROOM,
176+
start_datetime=QUERY_START,
177+
end_datetime=QUERY_END,
178+
sample_rate=30,
179+
output_file="sensor_data.csv",
180+
)
181+
182+
with open("sensor_data.csv", "r") as f:
183+
csv_data = f.read()
184+
185+
stream = chat(
186+
model=MODEL,
187+
messages=[
188+
{
189+
"role": "user",
190+
"content": PROMPT.replace("%%_DATA_PLACEHOLDER_%%", csv_data),
191+
},
192+
],
193+
stream=True,
194+
)
195+
196+
for chunk in stream:
197+
print(chunk["message"]["content"], end="", flush=True)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
[Unit]
5+
Description=Python Script with Virtual Environment
6+
After=network.target
7+
Wants=network.target
8+
9+
[Service]
10+
Type=simple
11+
User=pi
12+
Group=pi
13+
WorkingDirectory=/home/pi/RaspberryPi_LLM_Sensor_Data
14+
Environment=PATH=/home/pi/venvs/sensor_llm_venv/bin
15+
ExecStart=/home/pi/RaspberryPi_LLM_Sensor_Data/start_service.sh
16+
Restart=always
17+
RestartSec=10
18+
19+
# Optional: Set environment variables
20+
Environment=PYTHONPATH=/home/pi/RaspberryPi_LLM_Sensor_Data
21+
Environment=PYTHONUNBUFFERED=1
22+
23+
# Optional: Logging
24+
StandardOutput=journal
25+
StandardError=journal
26+
SyslogIdentifier=Environmental-Sensor-Reader
27+
28+
# Optional: Security settings
29+
NoNewPrivileges=yes
30+
ProtectSystem=strict
31+
#ProtectHome=yes
32+
ReadWritePaths=/home/pi/RaspberryPi_LLM_Sensor_Data
33+
34+
[Install]
35+
WantedBy=multi-user.target
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/bash
2+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
3+
#
4+
# SPDX-License-Identifier: MIT
5+
cd /home/pi/RaspberryPi_LLM_Sensor_Data
6+
source /home/pi/venvs/sensor_llm_venv/bin/activate
7+
exec python /home/pi/RaspberryPi_LLM_Sensor_Data/take_sensor_readings.py

0 commit comments

Comments
 (0)