misc: codebase path cleanup
This commit is contained in:
@@ -836,7 +836,7 @@ Devices can announce themselves by publishing to the discovery topic:
|
|||||||
**Behavior**:
|
**Behavior**:
|
||||||
- New devices are automatically created with all sensors and actors
|
- New devices are automatically created with all sensors and actors
|
||||||
- Existing devices are updated, and sensors/actors are synced
|
- Existing devices are updated, and sensors/actors are synced
|
||||||
- Device status is set to OK (1) upon discovery
|
- Device status is set to OK (1) upon discovery (payload `device.status_id` is ignored)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -936,12 +936,12 @@ All endpoints may return the following standard error responses:
|
|||||||
|
|
||||||
### Device Status IDs
|
### Device Status IDs
|
||||||
|
|
||||||
| ID | Status | Description |
|
| ID | Status | Description |
|
||||||
|----|---------|--------------------------------|
|
|----|----------|--------------------------------------|
|
||||||
| 1 | OK | Device is online and functioning |
|
| 1 | OK | Device is online and functioning |
|
||||||
| 2 | Warning | Device has warnings |
|
| 2 | Pending | Device is initializing or awaiting |
|
||||||
| 3 | Error | Device has errors |
|
| 3 | Lost | Device has not checked in recently |
|
||||||
| 4 | Offline | Device is offline |
|
| 4 | Disabled | Device is administratively disabled |
|
||||||
|
|
||||||
### Date/Time Format
|
### Date/Time Format
|
||||||
|
|
||||||
@@ -949,7 +949,7 @@ All timestamps use RFC3339 format: `2026-01-14T10:00:00Z`
|
|||||||
|
|
||||||
### UUIDs
|
### UUIDs
|
||||||
|
|
||||||
UUIDs follow the standard UUID v4 format: `550e8400-e29b-41d4-a716-446655440000`
|
UUIDs follow the standard UUID format (any version), e.g. `550e8400-e29b-41d4-a716-446655440000`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DB wraps sql.DB
|
|
||||||
type DB struct {
|
|
||||||
conn *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default is the package-level DB used by handlers
|
|
||||||
var Default *DB
|
|
||||||
|
|
||||||
// Init opens the sqlite database at path and ensures the table exists
|
|
||||||
func Init(path string) (*DB, error) {
|
|
||||||
conn, err := sql.Open("sqlite3", path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// set reasonable pragmas
|
|
||||||
if _, err := conn.Exec("PRAGMA journal_mode=WAL;"); err != nil {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
schema := `CREATE TABLE IF NOT EXISTS messages (
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
timestamp TEXT NOT NULL
|
|
||||||
);`
|
|
||||||
if _, err := conn.Exec(schema); err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &DB{conn: conn}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefault sets the package default DB
|
|
||||||
func SetDefault(d *DB) {
|
|
||||||
Default = d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying connection
|
|
||||||
func (d *DB) Close() error {
|
|
||||||
if d == nil || d.conn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertMessage inserts a message with timestamp into the DB
|
|
||||||
func (d *DB) InsertMessage(msg string, ts time.Time) error {
|
|
||||||
if d == nil || d.conn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, err := d.conn.Exec("INSERT INTO messages(message, timestamp) VALUES(?, ?)", msg, ts.Format(time.RFC3339))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message is the returned message shape
|
|
||||||
type Message struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Timestamp string `json:"timestamp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryMessages returns messages ordered by newest first
|
|
||||||
func (d *DB) QueryMessages(limit, offset int) ([]Message, error) {
|
|
||||||
if d == nil || d.conn == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
rows, err := d.conn.Query("SELECT message, timestamp FROM messages ORDER BY rowid DESC LIMIT ? OFFSET ?", limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
res := []Message{}
|
|
||||||
for rows.Next() {
|
|
||||||
var m Message
|
|
||||||
if err := rows.Scan(&m.Message, &m.Timestamp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res = append(res, m)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ This repository contains a minimal Go REST service used as the backend for the L
|
|||||||
|
|
||||||
Repository: https://git.piskot.si/SeminarM2/lambdaiot-core
|
Repository: https://git.piskot.si/SeminarM2/lambdaiot-core
|
||||||
|
|
||||||
## Features ✅
|
## Features
|
||||||
|
|
||||||
- HTTP API with health, greeting, auth, device CRUD, sensor creation, and MQTT ping
|
- HTTP API with health, greeting, auth, device CRUD, sensor creation, and MQTT ping
|
||||||
- JWT-based auth middleware with demo login (`admin`/`password`)
|
- JWT-based auth middleware with demo login (`admin`/`password`)
|
||||||
@@ -14,7 +14,7 @@ Repository: https://git.piskot.si/SeminarM2/lambdaiot-core
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quickstart 🔧
|
## Quickstart
|
||||||
|
|
||||||
Build and run locally:
|
Build and run locally:
|
||||||
|
|
||||||
@@ -46,30 +46,4 @@ Integration stack (MySQL + Mosquitto + server):
|
|||||||
```bash
|
```bash
|
||||||
cd test
|
cd test
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
The compose file seeds the database from `ai-improved.sql` and exposes:
|
|
||||||
- API on http://localhost:8080
|
|
||||||
- MySQL on localhost:3306 (root/rootpass)
|
|
||||||
- Mosquitto on localhost:1883
|
|
||||||
- phpMyAdmin on http://localhost:8081
|
|
||||||
|
|
||||||
### Endpoints
|
|
||||||
|
|
||||||
- `GET /health` — returns `{ "status": "ok" }`
|
|
||||||
- `GET /hello` — greeting JSON
|
|
||||||
- `POST /login` — demo login, body `{ "username": "admin", "password": "password" }`, returns `{ "token": "..." }`
|
|
||||||
- `GET /protected` — JWT required
|
|
||||||
- `GET /devices` — list devices (public) / `POST /devices`, `GET/PUT/DELETE /devices/:id` (JWT)
|
|
||||||
- Sensors (JWT): `GET /sensors`, `GET /sensors/:id`, `POST /sensors`, `PUT /sensors/:id`, `DELETE /sensors/:id`
|
|
||||||
- Actors (JWT): `GET /actors`, `GET /actors/:id`, `POST /actors`, `PUT /actors/:id`, `DELETE /actors/:id`
|
|
||||||
- Sensor readings (JWT): `GET /sensor-readings`, `GET /sensor-readings/:id`, `POST /sensor-readings`, `PUT /sensor-readings/:id`, `DELETE /sensor-readings/:id` (optional `sensor_id` filter and pagination via `limit`, `page`)
|
|
||||||
- `GET /mqttping` — publish timestamp to MQTT default topic (JWT)
|
|
||||||
|
|
||||||
### Environment
|
|
||||||
|
|
||||||
- `JWT_SECRET` — (optional) secret used to sign tokens; defaults to `secret` for local/dev use
|
|
||||||
- `MQTT_*` — configure broker, client ID, topic, username/password
|
|
||||||
- `DB_*` — MySQL connection overrides; see `config.toml` defaults in `internal/config`
|
|
||||||
|
|
||||||
---
|
|
||||||
+49
-7
@@ -40,6 +40,23 @@ def parse_actor_value(value):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def parse_actor_float(value):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return 1.0 if value else 0.0
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value.strip().lower() in {"true", "on"}:
|
||||||
|
return 1.0
|
||||||
|
if value.strip().lower() in {"false", "off"}:
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError:
|
||||||
|
return 0.0
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path: str) -> dict:
|
def load_env_file(path: str) -> dict:
|
||||||
env = {}
|
env = {}
|
||||||
try:
|
try:
|
||||||
@@ -60,10 +77,14 @@ class DeviceEmulator:
|
|||||||
self.mac_address = generate_mac()
|
self.mac_address = generate_mac()
|
||||||
self.device_id = mac_to_device_uuid(self.mac_address)
|
self.device_id = mac_to_device_uuid(self.mac_address)
|
||||||
self.sensor_id = uuid.uuid5(self.device_id, "sensor-0")
|
self.sensor_id = uuid.uuid5(self.device_id, "sensor-0")
|
||||||
|
self.sensor_bool_id = uuid.uuid5(self.device_id, "sensor-1")
|
||||||
self.actor_id = uuid.uuid5(self.device_id, "actor-0")
|
self.actor_id = uuid.uuid5(self.device_id, "actor-0")
|
||||||
|
self.actor_float_id = uuid.uuid5(self.device_id, "actor-1")
|
||||||
|
|
||||||
self.actor_value = 0
|
self.actor_value = 0
|
||||||
|
self.actor_float_value = 0.0
|
||||||
self.temperature = 22.5
|
self.temperature = 22.5
|
||||||
|
self.switch_state = False
|
||||||
self.publish_interval = 5
|
self.publish_interval = 5
|
||||||
|
|
||||||
env_path = os.path.join(os.path.dirname(__file__), ".env")
|
env_path = os.path.join(os.path.dirname(__file__), ".env")
|
||||||
@@ -93,6 +114,12 @@ class DeviceEmulator:
|
|||||||
"name": "Temperature",
|
"name": "Temperature",
|
||||||
"type": "DHT22",
|
"type": "DHT22",
|
||||||
"data_type_id": 2,
|
"data_type_id": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(self.sensor_bool_id),
|
||||||
|
"name": "Presence",
|
||||||
|
"type": "PIR",
|
||||||
|
"data_type_id": 1,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"actors": [
|
"actors": [
|
||||||
@@ -101,6 +128,12 @@ class DeviceEmulator:
|
|||||||
"name": "Binary Switch",
|
"name": "Binary Switch",
|
||||||
"type": "Relay",
|
"type": "Relay",
|
||||||
"data_type_id": 1,
|
"data_type_id": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": str(self.actor_float_id),
|
||||||
|
"name": "Dimmer",
|
||||||
|
"type": "PWM",
|
||||||
|
"data_type_id": 2,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -121,15 +154,16 @@ class DeviceEmulator:
|
|||||||
self.client.publish(TOPIC, json.dumps(payload), qos=0)
|
self.client.publish(TOPIC, json.dumps(payload), qos=0)
|
||||||
print(f"sensor_reading published: {self.temperature}C")
|
print(f"sensor_reading published: {self.temperature}C")
|
||||||
|
|
||||||
def publish_actor_state(self):
|
def publish_bool_sensor_reading(self):
|
||||||
|
self.switch_state = not self.switch_state
|
||||||
payload = {
|
payload = {
|
||||||
"type": "actor_state",
|
"type": "sensor_reading",
|
||||||
"actor_id": str(self.actor_id),
|
"sensor_id": str(self.sensor_bool_id),
|
||||||
"value": self.actor_value,
|
"value": self.switch_state,
|
||||||
"updated_at": now_rfc3339(),
|
"value_at": now_rfc3339(),
|
||||||
}
|
}
|
||||||
self.client.publish(TOPIC, json.dumps(payload), qos=0)
|
self.client.publish(TOPIC, json.dumps(payload), qos=0)
|
||||||
print(f"actor_state published: {self.actor_value}")
|
print(f"sensor_reading published: presence={self.switch_state}")
|
||||||
|
|
||||||
def publish_device_state(self):
|
def publish_device_state(self):
|
||||||
payload = {
|
payload = {
|
||||||
@@ -164,11 +198,18 @@ class DeviceEmulator:
|
|||||||
if payload.get("sensor_id") == str(self.sensor_id):
|
if payload.get("sensor_id") == str(self.sensor_id):
|
||||||
self.publish_sensor_reading()
|
self.publish_sensor_reading()
|
||||||
return
|
return
|
||||||
|
if payload.get("sensor_id") == str(self.sensor_bool_id):
|
||||||
|
self.publish_bool_sensor_reading()
|
||||||
|
return
|
||||||
|
|
||||||
if msg_type == "actor_command":
|
if msg_type == "actor_command":
|
||||||
if payload.get("actor_id") == str(self.actor_id):
|
if payload.get("actor_id") == str(self.actor_id):
|
||||||
self.actor_value = parse_actor_value(payload.get("value"))
|
self.actor_value = parse_actor_value(payload.get("value"))
|
||||||
self.publish_actor_state()
|
print(f"actor_command received; state={self.actor_value}")
|
||||||
|
return
|
||||||
|
if payload.get("actor_id") == str(self.actor_float_id):
|
||||||
|
self.actor_float_value = parse_actor_float(payload.get("value"))
|
||||||
|
print(f"actor_command received; dimmer={self.actor_float_value}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -178,6 +219,7 @@ class DeviceEmulator:
|
|||||||
while True:
|
while True:
|
||||||
time.sleep(self.publish_interval)
|
time.sleep(self.publish_interval)
|
||||||
self.publish_sensor_reading()
|
self.publish_sensor_reading()
|
||||||
|
self.publish_bool_sensor_reading()
|
||||||
finally:
|
finally:
|
||||||
self.client.loop_stop()
|
self.client.loop_stop()
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ services:
|
|||||||
- MYSQL_ROOT_PASSWORD=rootpass
|
- MYSQL_ROOT_PASSWORD=rootpass
|
||||||
- MYSQL_DATABASE=lambdaiot
|
- MYSQL_DATABASE=lambdaiot
|
||||||
volumes:
|
volumes:
|
||||||
- ../ai-improved.sql:/docker-entrypoint-initdb.d/init.sql
|
- ./sql-template.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"]
|
||||||
interval: 2s
|
interval: 2s
|
||||||
|
|||||||
Reference in New Issue
Block a user