Compare commits

..
3 Commits
Author SHA1 Message Date
mateusz b2d268c5a0 server production version - gunicorn 2026-09-20 21:07:10 +02:00
mateusz ceb2431f8e warsaw timezone 2026-09-20 18:24:32 +02:00
mateusz 29a65a6f9d avoid corruptet first readout 2026-09-20 18:24:09 +02:00
10 changed files with 68 additions and 15 deletions
+22
View File
@@ -48,12 +48,34 @@ python seed.py
## Running ## Running
### Development
```bash ```bash
python app.py python app.py
``` ```
The server starts on `http://localhost:5000`. The server starts on `http://localhost:5000`.
### Production
```bash
# Install dependencies (includes gunicorn)
pip install -r requirements.txt
# Run with gunicorn
gunicorn -c gunicorn.conf.py "app:create_app()"
```
Gunicorn starts on `http://0.0.0.0:8000` with `cpu * 2 + 1` workers.
#### systemd service
```bash
sudo cp yggdrasil.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now yggdrasil
```
| Route | Description | | Route | Description |
|---|---| |---|---|
| `/sensors` | Dashboard with interactive chart and data table | | `/sensors` | Dashboard with interactive chart and data table |
+2 -2
View File
@@ -48,7 +48,7 @@ def create_app():
readouts = query.order_by(SensorReadout.timestamp.desc()).limit(300).all() readouts = query.order_by(SensorReadout.timestamp.desc()).limit(300).all()
return jsonify( return jsonify(
[ [
{"value": r.value, "timestamp": r.timestamp.isoformat()} {"value": r.value, "timestamp": r.timestamp.isoformat() + "Z"}
for r in readouts for r in readouts
] ]
) )
@@ -124,7 +124,7 @@ def create_app():
"sensor_id": readout.sensor_id, "sensor_id": readout.sensor_id,
"type": readout.type, "type": readout.type,
"value": readout.value, "value": readout.value,
"timestamp": readout.timestamp.isoformat(), "timestamp": readout.timestamp.isoformat() + "Z",
} }
), 201 ), 201
+9
View File
@@ -0,0 +1,9 @@
import os
bind = "0.0.0.0:8000"
workers = min(os.cpu_count() * 2 + 1, 9)
timeout = 30
keepalive = 5
errorlog = "-"
accesslog = "-"
loglevel = "info"
+2 -1
View File
@@ -1,3 +1,4 @@
flask>=3.0 flask>=3.0
flask-sqlalchemy>=3.1 flask-sqlalchemy>=3.1
alembic>=1.13 alembic>=1.13
gunicorn>=22.0
+7 -6
View File
@@ -61,11 +61,12 @@
function formatTimestamp(iso) { function formatTimestamp(iso) {
const d = new Date(iso); const d = new Date(iso);
const pad = (n) => String(n).padStart(2, '0'); return d.toLocaleString('pl-PL', { timeZone: 'Europe/Warsaw' });
const months = ['Jan','Feb','Mar','Apr','May','Jun', }
'Jul','Aug','Sep','Oct','Nov','Dec'];
return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}, ` + function formatChartLabel(iso) {
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; const d = new Date(iso + 'Z');
return d.toLocaleString('pl-PL', { timeZone: 'Europe/Warsaw', hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', year: 'numeric' });
} }
async function fetchJSON(url) { async function fetchJSON(url) {
@@ -114,7 +115,7 @@
// fetch hourly data for chart // fetch hourly data for chart
const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${type}`); const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${type}`);
const labels = hourly.map(r => r.timestamp.replace('T', ' ')); const labels = hourly.map(r => formatChartLabel(r.timestamp));
const values = hourly.map(r => r.value); const values = hourly.map(r => r.value);
if (chart) chart.destroy(); if (chart) chart.destroy();
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Yggdrasil Sensor API
After=network.target
[Service]
User=mateusz
Group=mateusz
WorkingDirectory=/home/mateusz/prog/yggdrasil/server
ExecStart=/home/mateusz/prog/yggdrasil/server/.venv/bin/gunicorn -c gunicorn.conf.py "app:create_app()"
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+4 -1
View File
@@ -18,9 +18,12 @@ static const char *TAG = "thermo_sensor";
bmp280_t dev_bmp_280; bmp280_t dev_bmp_280;
void main_loop(void *pvParameters) { void main_loop(void *pvParameters) {
// Wait 1s, to avoid corrupted first sensor readout
vTaskDelay(pdMS_TO_TICKS(1000));
while (1) { while (1) {
ygg_bmp280_read_sensor(&dev_bmp_280); ygg_bmp280_read_sensor(&dev_bmp_280);
ygg_send_sensor_readouts(SENSOR_NAME, ygg_send_sensor_readouts(BASE_URL,
SENSOR_NAME,
CURR_READOUT); CURR_READOUT);
vTaskDelay(pdMS_TO_TICKS(10000)); vTaskDelay(pdMS_TO_TICKS(10000));
} }
+4 -3
View File
@@ -106,13 +106,14 @@ char* ygg_send_post(const char *url, char *post_data) {
int ygg_get_sensor_id(const char *sensor_name, int ygg_get_sensor_id(const char *sensor_name,
const char *url); const char *url);
void ygg_send_sensor_readouts(const char *sensor_name, void ygg_send_sensor_readouts(const char *base_url,
const char *sensor_name,
ygg_sensor_readout readout) { ygg_sensor_readout readout) {
char post_data[64]; char post_data[64];
int sensor_id = ygg_get_sensor_id(sensor_name, "http://192.168.0.236:5000"); int sensor_id = ygg_get_sensor_id(sensor_name, base_url);
char url[128]; char url[128];
snprintf(url, sizeof(url), snprintf(url, sizeof(url),
"http://192.168.0.236:5000/api/sensors/%d/readouts", sensor_id); "%s/api/sensors/%d/readouts", base_url, sensor_id);
snprintf(post_data, sizeof(post_data), snprintf(post_data, sizeof(post_data),
"{\"type\":\"temperature\", \"value\":%.2f}", readout.temperature); "{\"type\":\"temperature\", \"value\":%.2f}", readout.temperature);
+2 -1
View File
@@ -3,7 +3,8 @@
#include "ygg_state.h" #include "ygg_state.h"
void ygg_send_sensor_readouts(const char *sensor_name, void ygg_send_sensor_readouts(const char *base_url,
const char *sensor_name,
ygg_sensor_readout readout); ygg_sensor_readout readout);
#endif #endif
+2 -1
View File
@@ -8,6 +8,7 @@ typedef struct {
} ygg_sensor_readout; } ygg_sensor_readout;
extern ygg_sensor_readout CURR_READOUT; extern ygg_sensor_readout CURR_READOUT;
static const char *SENSOR_NAME = "BMP_280_SENSOR_001"; static const char *SENSOR_NAME = "BMP_280";
static const char *BASE_URL = "http://192.168.0.236:5000";
#endif #endif