feat(db): add battery REST endpoint
Get latest value of battery for each node Assisted-by: Junie:claude-sonnet-4.6 Signed-off-by: Klagarge <remi@heredero.ch>
This commit is contained in:
@@ -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}}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user