diff --git a/db/get-db.http b/db/get-db.http index 3478a90..f4acd9e 100644 --- a/db/get-db.http +++ b/db/get-db.http @@ -15,3 +15,7 @@ Authorization: Basic {{username}} {{password}} ### GET all rooms GET {{host}}/api/v1/rooms Authorization: Basic {{username}} {{password}} + +### GET battery status of all devices +GET {{host}}/api/v1/battery +Authorization: Basic {{username}} {{password}} diff --git a/db/src/docs/docs.go b/db/src/docs/docs.go index 481b026..1ff46ad 100644 --- a/db/src/docs/docs.go +++ b/db/src/docs/docs.go @@ -15,6 +15,47 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/battery": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get the last battery level for each node grouped by room", + "produces": [ + "application/json" + ], + "tags": [ + "battery" + ], + "summary": "Get last battery level for each node", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/rooms": { "get": { "security": [ diff --git a/db/src/docs/swagger.json b/db/src/docs/swagger.json index 83b9b0c..377bbcb 100644 --- a/db/src/docs/swagger.json +++ b/db/src/docs/swagger.json @@ -9,6 +9,47 @@ "host": "api.db.e.kb28.ch", "basePath": "/api/v1", "paths": { + "/battery": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Get the last battery level for each node grouped by room", + "produces": [ + "application/json" + ], + "tags": [ + "battery" + ], + "summary": "Get last battery level for each node", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/rooms": { "get": { "security": [ diff --git a/db/src/docs/swagger.yaml b/db/src/docs/swagger.yaml index 97e9e8c..9cd7731 100644 --- a/db/src/docs/swagger.yaml +++ b/db/src/docs/swagger.yaml @@ -6,6 +6,32 @@ info: title: Gateway API version: "1.0" paths: + /battery: + get: + description: Get the last battery level for each node grouped by room + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + additionalProperties: + additionalProperties: true + type: object + type: object + type: object + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string + type: object + security: + - BasicAuth: [] + summary: Get last battery level for each node + tags: + - battery /rooms: get: description: Get a list of all unique rooms from the measurement diff --git a/db/src/mapping.go b/db/src/mapping.go index b2f77c9..e7ca109 100644 --- a/db/src/mapping.go +++ b/db/src/mapping.go @@ -90,6 +90,15 @@ func (c *MappingConfig) NodesForRoom(room string) []string { return nodes } +// AllNodes returns all node IDs defined in the mapping. +func (c *MappingConfig) AllNodes() []string { + nodes := make([]string, 0, len(c.nodeToRoom)) + for nodeID := range c.nodeToRoom { + nodes = append(nodes, nodeID) + } + return nodes +} + // Rooms returns the list of all room names defined in the mapping. func (c *MappingConfig) Rooms() []string { seen := make(map[string]struct{}) diff --git a/db/src/rest/rest.go b/db/src/rest/rest.go index 3d22092..67d5654 100644 --- a/db/src/rest/rest.go +++ b/db/src/rest/rest.go @@ -20,6 +20,8 @@ import ( type RoomMapper interface { Rooms() []string NodesForRoom(room string) []string + AllNodes() []string + GetRoom(nodeID string) (string, bool) } type RestGateway struct { @@ -71,6 +73,7 @@ func (g *RestGateway) setupRoutes() { v1.GET("/rooms", g.getRooms) v1.GET("/rooms/:room-id/current", g.getRoomCurrent) v1.GET("/rooms/:room-id/history", g.getRoomHistory) + v1.GET("/battery", g.getBattery) } g.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) @@ -85,6 +88,77 @@ func buildNodeFilter(nodes []string) string { return "(" + strings.Join(parts, " OR ") + ")" } +// GET /api/v1/battery +// getBattery godoc +// @Summary Get last battery level for each node +// @Description Get the last battery level for each node grouped by room +// @Tags battery +// @Produce json +// @Success 200 {object} map[string]map[string]map[string]any +// @Failure 500 {object} map[string]string +// @Security BasicAuth +// @Router /battery [get] +func (g *RestGateway) getBattery(c *gin.Context) { + allNodes := g.mapping.AllNodes() + if len(allNodes) == 0 { + c.JSON(http.StatusOK, gin.H{}) + return + } + + nodeFilter := buildNodeFilter(allNodes) + query := fmt.Sprintf( + `SELECT "node", "battery" FROM "%s" WHERE %s AND time > now() - INTERVAL '3 hour' ORDER BY time DESC`, + g.measurementName, nodeFilter, + ) + + it, err := g.influxGateway.Query(context.Background(), query) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Collect last battery per node (first occurrence = most recent due to DESC order) + nodeBattery := make(map[string]any) + for it.Next() { + row := it.Value() + nodeVal, ok := row["node"] + if !ok { + continue + } + nodeID, ok := nodeVal.(string) + if !ok { + continue + } + if _, seen := nodeBattery[nodeID]; !seen { + nodeBattery[nodeID] = row["battery"] + } + } + + if err := it.Err(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Group by room + result := make(map[string]map[string]map[string]any) + for _, nodeID := range allNodes { + room, ok := g.mapping.GetRoom(nodeID) + if !ok { + room = "not-attributed" + } + if result[room] == nil { + result[room] = make(map[string]map[string]any) + } + battery, hasData := nodeBattery[nodeID] + if !hasData { + battery = "offline (3h)" + } + result[room][nodeID] = map[string]any{"battery": battery} + } + + c.JSON(http.StatusOK, result) +} + func (g *RestGateway) Run(addr string) error { return g.engine.Run(addr) }