diff --git a/API.md b/API.md index 9ba4d46..3b09579 100644 --- a/API.md +++ b/API.md @@ -836,7 +836,7 @@ Devices can announce themselves by publishing to the discovery topic: **Behavior**: - New devices are automatically created with all sensors and actors - 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 -| ID | Status | Description | -|----|---------|--------------------------------| -| 1 | OK | Device is online and functioning | -| 2 | Warning | Device has warnings | -| 3 | Error | Device has errors | -| 4 | Offline | Device is offline | +| ID | Status | Description | +|----|----------|--------------------------------------| +| 1 | OK | Device is online and functioning | +| 2 | Pending | Device is initializing or awaiting | +| 3 | Lost | Device has not checked in recently | +| 4 | Disabled | Device is administratively disabled | ### Date/Time Format @@ -949,7 +949,7 @@ All timestamps use RFC3339 format: `2026-01-14T10:00:00Z` ### 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`. --- diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go deleted file mode 100644 index be2ce49..0000000 --- a/internal/storage/sqlite.go +++ /dev/null @@ -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 -} diff --git a/readme.md b/readme.md index dfc4e30..11e29cf 100644 --- a/readme.md +++ b/readme.md @@ -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 -## Features ✅ +## Features - HTTP API with health, greeting, auth, device CRUD, sensor creation, and MQTT ping - 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: @@ -46,30 +46,4 @@ Integration stack (MySQL + Mosquitto + server): ```bash cd test 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` - ---- \ No newline at end of file +``` \ No newline at end of file diff --git a/test/device_emulator.py b/test/device_emulator.py index 6c3d4d8..76d3104 100644 --- a/test/device_emulator.py +++ b/test/device_emulator.py @@ -40,6 +40,23 @@ def parse_actor_value(value): 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: env = {} try: @@ -60,10 +77,14 @@ class DeviceEmulator: self.mac_address = generate_mac() self.device_id = mac_to_device_uuid(self.mac_address) 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_float_id = uuid.uuid5(self.device_id, "actor-1") self.actor_value = 0 + self.actor_float_value = 0.0 self.temperature = 22.5 + self.switch_state = False self.publish_interval = 5 env_path = os.path.join(os.path.dirname(__file__), ".env") @@ -93,6 +114,12 @@ class DeviceEmulator: "name": "Temperature", "type": "DHT22", "data_type_id": 2, + }, + { + "id": str(self.sensor_bool_id), + "name": "Presence", + "type": "PIR", + "data_type_id": 1, } ], "actors": [ @@ -101,6 +128,12 @@ class DeviceEmulator: "name": "Binary Switch", "type": "Relay", "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) 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 = { - "type": "actor_state", - "actor_id": str(self.actor_id), - "value": self.actor_value, - "updated_at": now_rfc3339(), + "type": "sensor_reading", + "sensor_id": str(self.sensor_bool_id), + "value": self.switch_state, + "value_at": now_rfc3339(), } 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): payload = { @@ -164,11 +198,18 @@ class DeviceEmulator: if payload.get("sensor_id") == str(self.sensor_id): self.publish_sensor_reading() return + if payload.get("sensor_id") == str(self.sensor_bool_id): + self.publish_bool_sensor_reading() + return if msg_type == "actor_command": if payload.get("actor_id") == str(self.actor_id): 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 def run(self): @@ -178,6 +219,7 @@ class DeviceEmulator: while True: time.sleep(self.publish_interval) self.publish_sensor_reading() + self.publish_bool_sensor_reading() finally: self.client.loop_stop() diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 583f795..6a5aac7 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -10,7 +10,7 @@ services: - MYSQL_ROOT_PASSWORD=rootpass - MYSQL_DATABASE=lambdaiot volumes: - - ../ai-improved.sql:/docker-entrypoint-initdb.d/init.sql + - ./sql-template.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"] interval: 2s diff --git a/ai-improved.sql b/test/sql-template.sql similarity index 100% rename from ai-improved.sql rename to test/sql-template.sql