server + getting sensor id + json in esp32
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
build
|
||||
managed_components
|
||||
*.swp
|
||||
*.pyc
|
||||
*.db
|
||||
.cache
|
||||
.vscode
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# Sensor Readouts
|
||||
|
||||
A Flask web application for managing and visualizing sensor data. It provides a REST API for creating sensors and storing readouts, along with a frontend dashboard that displays real-time charts and tabular data.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Python 3.13+**
|
||||
- **Flask 3** — web framework
|
||||
- **SQLAlchemy** (via Flask-SQLAlchemy) — ORM
|
||||
- **SQLite** — database
|
||||
- **Alembic** — database migrations
|
||||
- **Chart.js** — frontend charting library
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
server/
|
||||
├── app.py # Flask application factory and API routes
|
||||
├── models.py # SQLAlchemy models (Sensor, SensorReadout)
|
||||
├── seed.py # Database seeder with sample data
|
||||
├── requirements.txt # Python dependencies
|
||||
├── data.db # SQLite database (auto-created)
|
||||
├── alembic.ini # Alembic configuration
|
||||
├── alembic/ # Migration scripts
|
||||
│ ├── env.py
|
||||
│ └── versions/
|
||||
└── templates/
|
||||
├── sensors.html # Dashboard with chart and table
|
||||
└── docs.html # API documentation page
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Create and activate virtual environment
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run database migrations
|
||||
alembic upgrade head
|
||||
|
||||
# (Optional) Seed the database with sample data
|
||||
python seed.py
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
The server starts on `http://localhost:5000`.
|
||||
|
||||
| Route | Description |
|
||||
|---|---|
|
||||
| `/sensors` | Dashboard with interactive chart and data table |
|
||||
| `/api/docs` | API documentation with curl examples |
|
||||
|
||||
## Database
|
||||
|
||||
Two tables are used:
|
||||
|
||||
**sensor**
|
||||
| Column | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | Integer | Primary key |
|
||||
| `name` | String(128) | Unique sensor name |
|
||||
| `created_at` | DateTime | Creation timestamp (UTC) |
|
||||
|
||||
**sensor_readout**
|
||||
| Column | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | Integer | Primary key |
|
||||
| `sensor_id` | Integer | Foreign key to `sensor.id` |
|
||||
| `type` | String(64) | Readout type (e.g. `temperature`, `pressure`, `humidity`) |
|
||||
| `value` | Float | Measured value |
|
||||
| `timestamp` | DateTime | Readout timestamp (UTC) |
|
||||
|
||||
### Migrations
|
||||
|
||||
```bash
|
||||
# Create a new migration after model changes
|
||||
alembic revision --autogenerate -m "description"
|
||||
|
||||
# Apply pending migrations
|
||||
alembic upgrade head
|
||||
|
||||
# Roll back one step
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Base URL: `http://localhost:5000/api`
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/sensors` | List all sensors |
|
||||
| `POST` | `/api/sensors` | Create a new sensor |
|
||||
| `GET` | `/api/sensors/:id/types` | List readout types for a sensor |
|
||||
| `GET` | `/api/sensors/:id/readouts` | Latest readouts (max 300, filterable by `?type=`) |
|
||||
| `GET` | `/api/sensors/:id/readouts/hourly` | Hourly averaged readouts (filterable by `?type=`) |
|
||||
| `POST` | `/api/sensors/:id/readouts` | Add a new readout |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# List sensors
|
||||
curl http://localhost:5000/api/sensors
|
||||
|
||||
# Create a sensor
|
||||
curl -X POST http://localhost:5000/api/sensors \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "bmp280"}'
|
||||
|
||||
# Get temperature readouts for sensor 1
|
||||
curl "http://localhost:5000/api/sensors/1/readouts?type=temperature"
|
||||
|
||||
# Get hourly averaged data
|
||||
curl "http://localhost:5000/api/sensors/1/readouts/hourly?type=temperature"
|
||||
|
||||
# Add a readout
|
||||
curl -X POST http://localhost:5000/api/sensors/1/readouts \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "temperature", "value": 22.5}'
|
||||
```
|
||||
|
||||
Full API documentation with request/response examples is available at `/api/docs`.
|
||||
|
||||
## Frontend
|
||||
|
||||
The dashboard at `/sensors` provides:
|
||||
|
||||
- **Sensor dropdown** — select a sensor from the list
|
||||
- **Type dropdown** — select a readout type (populated based on the chosen sensor)
|
||||
- **Line chart** — hourly averaged values rendered with Chart.js
|
||||
- **Data table** — raw readouts with formatted timestamps (latest 300 entries)
|
||||
@@ -0,0 +1,149 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = %(here)s/alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
# sqlalchemy.url is set dynamically in env.py from the Flask app config
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
# Add the project root to sys.path so that models can be imported.
|
||||
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from app import create_app
|
||||
from models import db
|
||||
|
||||
# Create the Flask application to get the database URI.
|
||||
app = create_app()
|
||||
|
||||
# Alembic Config object.
|
||||
config = context.config
|
||||
|
||||
# Override the sqlalchemy.url with the value from the Flask config.
|
||||
config.set_main_option("sqlalchemy.url", app.config["SQLALCHEMY_DATABASE_URI"])
|
||||
|
||||
# Set up Python logging from the alembic.ini [loggers] section.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# Target metadata for 'autogenerate' support.
|
||||
target_metadata = db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
Configures the context with just a URL and not an Engine.
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
Creates an Engine and associates a connection with the context.
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,48 @@
|
||||
"""initial tables
|
||||
|
||||
Revision ID: 7f92bfc47dfc
|
||||
Revises:
|
||||
Create Date: 2026-09-11 17:08:42.232582
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '7f92bfc47dfc'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('sensor',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=128), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('sensor_readout',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('sensor_id', sa.Integer(), nullable=False),
|
||||
sa.Column('type', sa.String(length=64), nullable=False),
|
||||
sa.Column('value', sa.Float(), nullable=False),
|
||||
sa.Column('timestamp', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['sensor_id'], ['sensor.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('sensor_readout')
|
||||
op.drop_table('sensor')
|
||||
# ### end Alembic commands ###
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
from sqlalchemy import func
|
||||
|
||||
from models import Sensor, SensorReadout, db
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
db_path = os.path.join(os.path.dirname(__file__), "data.db")
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
@app.route("/sensors")
|
||||
def sensors_page():
|
||||
return render_template("sensors.html")
|
||||
|
||||
@app.route("/api/docs")
|
||||
def api_docs():
|
||||
return render_template("docs.html")
|
||||
|
||||
@app.route("/api/sensors")
|
||||
def api_sensors():
|
||||
sensors = Sensor.query.order_by(Sensor.name).all()
|
||||
return jsonify([{"id": s.id, "name": s.name} for s in sensors])
|
||||
|
||||
@app.route("/api/sensors/<int:sensor_id>/types")
|
||||
def api_sensor_types(sensor_id):
|
||||
types = (
|
||||
db.session.query(SensorReadout.type)
|
||||
.filter_by(sensor_id=sensor_id)
|
||||
.distinct()
|
||||
.order_by(SensorReadout.type)
|
||||
.all()
|
||||
)
|
||||
return jsonify([t[0] for t in types])
|
||||
|
||||
@app.route("/api/sensors/<int:sensor_id>/readouts")
|
||||
def api_sensor_readouts(sensor_id):
|
||||
type_ = request.args.get("type")
|
||||
query = SensorReadout.query.filter_by(sensor_id=sensor_id)
|
||||
if type_:
|
||||
query = query.filter_by(type=type_)
|
||||
readouts = query.order_by(SensorReadout.timestamp.desc()).limit(300).all()
|
||||
return jsonify(
|
||||
[
|
||||
{"value": r.value, "timestamp": r.timestamp.isoformat()}
|
||||
for r in readouts
|
||||
]
|
||||
)
|
||||
|
||||
@app.route("/api/sensors/<int:sensor_id>/readouts/hourly")
|
||||
def api_sensor_readouts_hourly(sensor_id):
|
||||
type_ = request.args.get("type")
|
||||
query = (
|
||||
db.session.query(
|
||||
func.strftime("%Y-%m-%dT%H:00:00", SensorReadout.timestamp).label("hour"),
|
||||
func.avg(SensorReadout.value).label("avg_value"),
|
||||
)
|
||||
.filter(SensorReadout.sensor_id == sensor_id)
|
||||
)
|
||||
if type_:
|
||||
query = query.filter(SensorReadout.type == type_)
|
||||
rows = (
|
||||
query.group_by("hour")
|
||||
.order_by("hour")
|
||||
.all()
|
||||
)
|
||||
return jsonify(
|
||||
[{"timestamp": r.hour, "value": round(r.avg_value, 2)} for r in rows]
|
||||
)
|
||||
|
||||
@app.route("/api/sensors", methods=["POST"])
|
||||
def api_create_sensor():
|
||||
data = request.get_json(silent=True)
|
||||
if not data or "name" not in data:
|
||||
return jsonify({"error": "name is required"}), 400
|
||||
|
||||
name = data["name"].strip()
|
||||
if not name:
|
||||
return jsonify({"error": "name cannot be empty"}), 400
|
||||
|
||||
if Sensor.query.filter_by(name=name).first():
|
||||
return jsonify({"error": "sensor already exists"}), 409
|
||||
|
||||
sensor = Sensor(name=name)
|
||||
db.session.add(sensor)
|
||||
db.session.commit()
|
||||
return jsonify({"id": sensor.id, "name": sensor.name}), 201
|
||||
|
||||
@app.route("/api/sensors/<int:sensor_id>/readouts", methods=["POST"])
|
||||
def api_create_readout(sensor_id):
|
||||
sensor = db.session.get(Sensor, sensor_id)
|
||||
if not sensor:
|
||||
return jsonify({"error": "sensor not found"}), 404
|
||||
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({"error": "request body is required"}), 400
|
||||
|
||||
missing = [f for f in ("type", "value") if f not in data]
|
||||
if missing:
|
||||
return jsonify({"error": f"missing fields: {', '.join(missing)}"}), 400
|
||||
|
||||
try:
|
||||
value = float(data["value"])
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"error": "value must be a number"}), 400
|
||||
|
||||
readout = SensorReadout(
|
||||
sensor_id=sensor_id,
|
||||
type=str(data["type"]).strip(),
|
||||
value=value,
|
||||
)
|
||||
db.session.add(readout)
|
||||
db.session.commit()
|
||||
return jsonify(
|
||||
{
|
||||
"id": readout.id,
|
||||
"sensor_id": readout.sensor_id,
|
||||
"type": readout.type,
|
||||
"value": readout.value,
|
||||
"timestamp": readout.timestamp.isoformat(),
|
||||
}
|
||||
), 201
|
||||
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = create_app()
|
||||
app.run(debug=True, host='0.0.0.0')
|
||||
@@ -0,0 +1,25 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class Sensor(db.Model):
|
||||
__tablename__ = "sensor"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False, unique=True)
|
||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
readouts = db.relationship("SensorReadout", backref="sensor", lazy=True)
|
||||
|
||||
|
||||
class SensorReadout(db.Model):
|
||||
__tablename__ = "sensor_readout"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sensor_id = db.Column(db.Integer, db.ForeignKey("sensor.id"), nullable=False)
|
||||
type = db.Column(db.String(64), nullable=False)
|
||||
value = db.Column(db.Float, nullable=False)
|
||||
timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
@@ -0,0 +1,3 @@
|
||||
flask>=3.0
|
||||
flask-sqlalchemy>=3.1
|
||||
alembic>=1.13
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Seed the database with sample sensor data."""
|
||||
|
||||
import random
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app import create_app
|
||||
from models import Sensor, SensorReadout, db
|
||||
|
||||
|
||||
def seed():
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
sensors = [
|
||||
Sensor(name="outdoor-station"),
|
||||
Sensor(name="indoor-unit"),
|
||||
Sensor(name="greenhouse-probe"),
|
||||
]
|
||||
db.session.add_all(sensors)
|
||||
db.session.flush()
|
||||
|
||||
types = ["temperature", "pressure", "humidity"]
|
||||
base_time = datetime.now(timezone.utc) - timedelta(seconds=30 * 300)
|
||||
|
||||
readouts = []
|
||||
for sensor in sensors:
|
||||
t = base_time
|
||||
for i in range(300):
|
||||
readouts.append(
|
||||
SensorReadout(
|
||||
sensor_id=sensor.id,
|
||||
type="temperature",
|
||||
value=round(random.uniform(18.0, 28.0), 2),
|
||||
timestamp=t,
|
||||
)
|
||||
)
|
||||
readouts.append(
|
||||
SensorReadout(
|
||||
sensor_id=sensor.id,
|
||||
type="pressure",
|
||||
value=round(random.uniform(1008.0, 1018.0), 2),
|
||||
timestamp=t,
|
||||
)
|
||||
)
|
||||
readouts.append(
|
||||
SensorReadout(
|
||||
sensor_id=sensor.id,
|
||||
type="humidity",
|
||||
value=round(random.uniform(35.0, 65.0), 2),
|
||||
timestamp=t,
|
||||
)
|
||||
)
|
||||
t += timedelta(seconds=30)
|
||||
|
||||
db.session.add_all(readouts)
|
||||
db.session.commit()
|
||||
|
||||
sensor_count = Sensor.query.count()
|
||||
readout_count = SensorReadout.query.count()
|
||||
print(f"Seeded {sensor_count} sensors and {readout_count} readouts.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed()
|
||||
@@ -0,0 +1,131 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Documentation - Sensor Readouts</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; max-width: 900px; margin: 2rem auto; line-height: 1.5; }
|
||||
h1 { border-bottom: 2px solid #333; padding-bottom: 0.5rem; }
|
||||
h2 { margin-top: 2rem; color: #333; }
|
||||
.endpoint { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 1rem 1.25rem; margin: 1rem 0; }
|
||||
.method { display: inline-block; font-weight: bold; padding: 0.15rem 0.5rem; border-radius: 3px; color: #fff; margin-right: 0.5rem; font-size: 0.85rem; }
|
||||
.get { background: #2563eb; }
|
||||
.post { background: #16a34a; }
|
||||
.path { font-family: monospace; font-size: 1rem; }
|
||||
.desc { margin: 0.5rem 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 0.5rem 0; }
|
||||
th, td { border: 1px solid #ccc; padding: 0.35rem 0.6rem; text-align: left; font-size: 0.9rem; }
|
||||
th { background: #f0f0f0; }
|
||||
pre { background: #1e1e1e; color: #d4d4d4; padding: 0.75rem; border-radius: 4px; overflow-x: auto; font-size: 0.85rem; }
|
||||
code { font-family: monospace; }
|
||||
.note { background: #fef9c3; border-left: 3px solid #eab308; padding: 0.5rem 0.75rem; margin: 1rem 0; font-size: 0.9rem; }
|
||||
.section-label { font-size: 0.8rem; color: #888; margin: 0.75rem 0 0.25rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Sensor Readouts — API Documentation</h1>
|
||||
|
||||
<h2>Base URL</h2>
|
||||
<p><code>http://localhost:5000/api</code></p>
|
||||
|
||||
<!-- GET /api/sensors -->
|
||||
<h2>GET /api/sensors</h2>
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span><span class="path">/api/sensors</span>
|
||||
<p class="desc">Return a list of all sensors ordered by name.</p>
|
||||
<p class="section-label">curl</p>
|
||||
<pre><code>curl http://localhost:5000/api/sensors</code></pre>
|
||||
<p class="section-label">Response 200</p>
|
||||
<pre><code>[
|
||||
{ "id": 1, "name": "outdoor-station" },
|
||||
{ "id": 2, "name": "indoor-unit" }
|
||||
]</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- POST /api/sensors -->
|
||||
<div class="endpoint">
|
||||
<span class="method post">POST</span><span class="path">/api/sensors</span>
|
||||
<p class="desc">Create a new sensor.</p>
|
||||
<p class="section-label">curl</p>
|
||||
<pre><code>curl -X POST http://localhost:5000/api/sensors \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "new-sensor"}'</code></pre>
|
||||
<p class="section-label">Response 201</p>
|
||||
<pre><code>{ "id": 5, "name": "new-sensor" }</code></pre>
|
||||
<p><strong>Errors:</strong></p>
|
||||
<table>
|
||||
<tr><th>Code</th><th>Condition</th></tr>
|
||||
<tr><td>400</td><td><code>name</code> is missing or empty</td></tr>
|
||||
<tr><td>409</td><td>Sensor with this name already exists</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- GET /api/sensors/:id/types -->
|
||||
<h2>GET /api/sensors/:id/types</h2>
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span><span class="path">/api/sensors/:id/types</span>
|
||||
<p class="desc">Return distinct readout types available for a given sensor.</p>
|
||||
<p class="section-label">curl</p>
|
||||
<pre><code>curl http://localhost:5000/api/sensors/1/types</code></pre>
|
||||
<p class="section-label">Response 200</p>
|
||||
<pre><code>["humidity", "pressure", "temperature"]</code></pre>
|
||||
<p><strong>Errors:</strong></p>
|
||||
<table>
|
||||
<tr><th>Code</th><th>Condition</th></tr>
|
||||
<tr><td>404</td><td>Sensor not found</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- GET /api/sensors/:id/readouts -->
|
||||
<h2>GET /api/sensors/:id/readouts</h2>
|
||||
<div class="endpoint">
|
||||
<span class="method get">GET</span><span class="path">/api/sensors/:id/readouts</span>
|
||||
<p class="desc">Return the latest readouts for a sensor. Optional <code>type</code> query parameter filters by readout type.</p>
|
||||
<p><strong>Query parameters:</strong></p>
|
||||
<table>
|
||||
<tr><th>Param</th><th>Type</th><th>Description</th></tr>
|
||||
<tr><td>type</td><td>string</td><td>Filter by type (e.g. <code>temperature</code>)</td></tr>
|
||||
</table>
|
||||
<p class="section-label">curl</p>
|
||||
<pre><code># all types
|
||||
curl http://localhost:5000/api/sensors/1/readouts
|
||||
|
||||
# filter by type
|
||||
curl "http://localhost:5000/api/sensors/1/readouts?type=temperature"</code></pre>
|
||||
<p class="section-label">Response 200</p>
|
||||
<pre><code>[
|
||||
{ "value": 24.31, "timestamp": "2026-09-11T10:30:00" },
|
||||
{ "value": 24.15, "timestamp": "2026-09-11T10:29:30" }
|
||||
]</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- POST /api/sensors/:id/readouts -->
|
||||
<div class="endpoint">
|
||||
<span class="method post">POST</span><span class="path">/api/sensors/:id/readouts</span>
|
||||
<p class="desc">Add a new readout to an existing sensor. Timestamp is generated server-side.</p>
|
||||
<p class="section-label">curl</p>
|
||||
<pre><code>curl -X POST http://localhost:5000/api/sensors/1/readouts \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "temperature", "value": 22.5}'</code></pre>
|
||||
<p class="section-label">Response 201</p>
|
||||
<pre><code>{
|
||||
"id": 2702,
|
||||
"sensor_id": 1,
|
||||
"type": "temperature",
|
||||
"value": 22.5,
|
||||
"timestamp": "2026-09-11T17:00:00.123456"
|
||||
}</code></pre>
|
||||
<p><strong>Errors:</strong></p>
|
||||
<table>
|
||||
<tr><th>Code</th><th>Condition</th></tr>
|
||||
<tr><td>400</td><td>Missing <code>type</code> or <code>value</code>, or <code>value</code> is not a number</td></tr>
|
||||
<tr><td>404</td><td>Sensor not found</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Note:</strong> All request and response bodies use <code>application/json</code>.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sensor Readouts</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||||
<style>
|
||||
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; }
|
||||
select, table { width: 100%; margin-top: 0.5rem; }
|
||||
select { padding: 0.4rem; }
|
||||
table { border-collapse: collapse; margin-top: 1rem; }
|
||||
th, td { border: 1px solid #ccc; padding: 0.4rem 0.6rem; text-align: left; }
|
||||
th { background: #f5f5f5; }
|
||||
label { font-weight: bold; }
|
||||
.hidden { display: none; }
|
||||
.chart-box { margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Sensor Readouts</h1>
|
||||
|
||||
<div>
|
||||
<label for="sensor-select">Sensor:</label>
|
||||
<select id="sensor-select">
|
||||
<option value="">-- select sensor --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="type-section" class="hidden">
|
||||
<label for="type-select">Type:</label>
|
||||
<select id="type-select">
|
||||
<option value="">-- select type --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="chart-section" class="hidden chart-box">
|
||||
<canvas id="chart"></canvas>
|
||||
</div>
|
||||
|
||||
<div id="readouts-section" class="hidden">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="readouts-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const sensorSelect = document.getElementById('sensor-select');
|
||||
const typeSelect = document.getElementById('type-select');
|
||||
const typeSection = document.getElementById('type-section');
|
||||
const chartSection = document.getElementById('chart-section');
|
||||
const readoutsSection = document.getElementById('readouts-section');
|
||||
const readoutsBody = document.getElementById('readouts-body');
|
||||
let chart = null;
|
||||
|
||||
function formatTimestamp(iso) {
|
||||
const d = new Date(iso);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const months = ['Jan','Feb','Mar','Apr','May','Jun',
|
||||
'Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}, ` +
|
||||
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
async function fetchJSON(url) {
|
||||
const res = await fetch(url);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadSensors() {
|
||||
const sensors = await fetchJSON('/api/sensors');
|
||||
sensors.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.textContent = s.name;
|
||||
sensorSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
sensorSelect.addEventListener('change', async () => {
|
||||
const sensorId = sensorSelect.value;
|
||||
typeSection.classList.add('hidden');
|
||||
chartSection.classList.add('hidden');
|
||||
readoutsSection.classList.add('hidden');
|
||||
typeSelect.innerHTML = '<option value="">-- select type --</option>';
|
||||
readoutsBody.innerHTML = '';
|
||||
|
||||
if (!sensorId) return;
|
||||
|
||||
const types = await fetchJSON(`/api/sensors/${sensorId}/types`);
|
||||
types.forEach(t => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t;
|
||||
opt.textContent = t;
|
||||
typeSelect.appendChild(opt);
|
||||
});
|
||||
typeSection.classList.remove('hidden');
|
||||
});
|
||||
|
||||
typeSelect.addEventListener('change', async () => {
|
||||
const sensorId = sensorSelect.value;
|
||||
const type = typeSelect.value;
|
||||
chartSection.classList.add('hidden');
|
||||
readoutsSection.classList.add('hidden');
|
||||
readoutsBody.innerHTML = '';
|
||||
|
||||
if (!type) return;
|
||||
|
||||
// fetch hourly data for chart
|
||||
const hourly = await fetchJSON(`/api/sensors/${sensorId}/readouts/hourly?type=${type}`);
|
||||
const labels = hourly.map(r => r.timestamp.replace('T', ' '));
|
||||
const values = hourly.map(r => r.value);
|
||||
|
||||
if (chart) chart.destroy();
|
||||
const ctx = document.getElementById('chart').getContext('2d');
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: type,
|
||||
data: values,
|
||||
borderColor: '#2563eb',
|
||||
backgroundColor: 'rgba(37,99,235,0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointRadius: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Time (hourly avg)' } },
|
||||
y: { title: { display: true, text: type } }
|
||||
}
|
||||
}
|
||||
});
|
||||
chartSection.classList.remove('hidden');
|
||||
|
||||
// fetch raw readouts for table
|
||||
const readouts = await fetchJSON(`/api/sensors/${sensorId}/readouts?type=${type}`);
|
||||
readouts.forEach(r => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `<td>${formatTimestamp(r.timestamp)}</td><td>${r.value}</td>`;
|
||||
readoutsBody.appendChild(tr);
|
||||
});
|
||||
readoutsSection.classList.remove('hidden');
|
||||
});
|
||||
|
||||
loadSensors();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -65,13 +65,24 @@ dependencies:
|
||||
- esp32s2
|
||||
- esp32s3
|
||||
version: 2.1.2
|
||||
espressif/cjson:
|
||||
component_hash: e788323270d90738662d66fffa910bfe1fba019bba087f01557e70c40485b469
|
||||
dependencies:
|
||||
- name: idf
|
||||
require: private
|
||||
version: '>=5.0'
|
||||
source:
|
||||
registry_url: https://components.espressif.com/
|
||||
type: service
|
||||
version: 1.7.19~2
|
||||
idf:
|
||||
source:
|
||||
type: idf
|
||||
version: 6.1.0
|
||||
direct_dependencies:
|
||||
- esp-idf-lib/bmp280
|
||||
- espressif/cjson
|
||||
- idf
|
||||
manifest_hash: e9139083c387eac863d68b0a593ed1c2631dc643670305c76e9d8ac18ee8f285
|
||||
manifest_hash: 2b2fa2812480ea444900bab732b40d55618df67e034421f1a347f9de48f70eeb
|
||||
target: esp32
|
||||
version: 3.0.0
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
idf_component_register(SRCS "thermo_sensor.c" "ygg_http_routes.c" "ygg_bmp280.c" "../../common/esp32/ygg_wifi.c" "../../common/esp32/ygg_http_server.c"
|
||||
idf_component_register(SRCS "thermo_sensor.c" "ygg_http_routes.c" "ygg_bmp280.c" "ygg_http_req.c" "../../common/esp32/ygg_wifi.c" "../../common/esp32/ygg_http_server.c"
|
||||
INCLUDE_DIRS "." "../../common/esp32")
|
||||
|
||||
@@ -15,3 +15,4 @@ dependencies:
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
||||
esp-idf-lib/bmp280: '*'
|
||||
espressif/cjson: '*'
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include <freertos/task.h>
|
||||
#include <esp_system.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
@@ -11,6 +10,7 @@
|
||||
#include "ygg_wifi.h"
|
||||
#include "ygg_http_server.h"
|
||||
#include "ygg_bmp280.h"
|
||||
#include "ygg_http_req.h"
|
||||
|
||||
|
||||
static const char *TAG = "thermo_sensor";
|
||||
@@ -21,8 +21,11 @@ bmp280_t dev_bmp_280;
|
||||
|
||||
void main_loop(void *pvParameters) {
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
ygg_bmp280_read_sensor(&dev_bmp_280);
|
||||
ygg_send_sensor_redouts(last_temperature,
|
||||
last_pressure,
|
||||
last_humidity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "cJSON.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "ygg_http_req.h"
|
||||
|
||||
#define GET_RESP_MAX_SIZE 4096
|
||||
|
||||
static const char *TAG = "YGG_HTTP_REQ";
|
||||
static char GET_RESP[GET_RESP_MAX_SIZE];
|
||||
|
||||
esp_err_t _http_event_handler(esp_http_client_event_t *evt) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char* ygg_send_get(const char *url) {
|
||||
size_t total = 0;
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
esp_err_t err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "HTTP open error: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_http_client_fetch_headers(client);
|
||||
|
||||
while (total < GET_RESP_MAX_SIZE - 1) {
|
||||
int len = esp_http_client_read(
|
||||
client,
|
||||
GET_RESP + total,
|
||||
GET_RESP_MAX_SIZE - 1 - total
|
||||
);
|
||||
if (len <= 0) {
|
||||
break;
|
||||
}
|
||||
total += len;
|
||||
}
|
||||
GET_RESP[total] = '\0';
|
||||
|
||||
ESP_LOGI(TAG, "Status HTTP = %d",
|
||||
esp_http_client_get_status_code(client));
|
||||
|
||||
ESP_LOGI(TAG, "Response: %s", GET_RESP);
|
||||
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
|
||||
return GET_RESP;
|
||||
}
|
||||
|
||||
void ygg_send_post(const char *url, char *post_data) {
|
||||
esp_http_client_config_t config = {
|
||||
.url = url,
|
||||
.event_handler = _http_event_handler,
|
||||
};
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
|
||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
esp_http_client_set_post_field(client, post_data, strlen(post_data));
|
||||
|
||||
esp_err_t err = esp_http_client_perform(client);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Status HTTP = %d, Content-Length = %"PRId64,
|
||||
esp_http_client_get_status_code(client),
|
||||
esp_http_client_get_content_length(client));
|
||||
} else {
|
||||
ESP_LOGE(TAG, "HTTP connection error: %s", esp_err_to_name(err));
|
||||
}
|
||||
esp_http_client_cleanup(client);
|
||||
}
|
||||
|
||||
int ygg_get_sensor_id(const char *sensor_name,
|
||||
const char *url);
|
||||
|
||||
void ygg_send_sensor_redouts(float temp, float press, float hum) {
|
||||
const char *url = "http://192.168.0.236:5000/api/sensors/1/readouts";
|
||||
char post_data[64];
|
||||
|
||||
int sensor_id = ygg_get_sensor_id("bmp280", "http://192.168.0.236:5000");
|
||||
|
||||
snprintf(post_data, sizeof(post_data),
|
||||
"{\"type\":\"temperature\", \"value\":%.2f}", temp);
|
||||
ygg_send_post(url, post_data);
|
||||
|
||||
snprintf(post_data, sizeof(post_data),
|
||||
"{\"type\":\"pressure\", \"value\":%.2f}", press);
|
||||
ygg_send_post(url, post_data);
|
||||
|
||||
snprintf(post_data, sizeof(post_data),
|
||||
"{\"type\":\"humidity\", \"value\":%.2f}", hum);
|
||||
ygg_send_post(url, post_data);
|
||||
}
|
||||
|
||||
char* ygg_get_sensors(const char *base_url) {
|
||||
char url[128];
|
||||
snprintf(url, sizeof(url),
|
||||
"%s/api/sensors", base_url);
|
||||
char *sensors = ygg_send_get(url);
|
||||
return sensors;
|
||||
}
|
||||
|
||||
int ygg_find_sensor(char *sensors, const char *sensor_name) {
|
||||
cJSON *root = cJSON_Parse(sensors);
|
||||
cJSON *item;
|
||||
|
||||
cJSON_ArrayForEach(item, root) {
|
||||
cJSON *id = cJSON_GetObjectItem(item, "id");
|
||||
cJSON *item_name = cJSON_GetObjectItem(item, "name");
|
||||
|
||||
if (cJSON_IsNumber(id) &&
|
||||
cJSON_IsString(item_name) &&
|
||||
strcmp(item_name->valuestring, sensor_name) == 0) {
|
||||
|
||||
int result = id->valueint;
|
||||
cJSON_Delete(root);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ygg_get_sensor_id(const char *sensor_name,
|
||||
const char *url) {
|
||||
char *sensors = ygg_get_sensors(url);
|
||||
int id = ygg_find_sensor(sensors, sensor_name);
|
||||
ESP_LOGI(TAG, "Sensor ID: %d", id);
|
||||
return 0;
|
||||
//if (id == -1) {
|
||||
// id = ygg_add_sensor(url, sensor_name);
|
||||
//}
|
||||
//return id;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef YGG_HTTP_REQ_H
|
||||
#define YGG_HTTP_REQ_H
|
||||
|
||||
void ygg_send_sensor_redouts(float temp, float press, float hum);
|
||||
|
||||
#endif
|
||||
@@ -3484,6 +3484,15 @@ CONFIG_I2CDEV_TIMEOUT=1000
|
||||
# default:
|
||||
# CONFIG_I2CDEV_NOLOCK is not set
|
||||
# end of I2C Device Library
|
||||
|
||||
#
|
||||
# cJSON
|
||||
#
|
||||
# default:
|
||||
CONFIG_CJSON_NESTING_LIMIT=1000
|
||||
# default:
|
||||
CONFIG_CJSON_CIRCULAR_LIMIT=10000
|
||||
# end of cJSON
|
||||
# end of Component config
|
||||
|
||||
# default:
|
||||
|
||||
Reference in New Issue
Block a user