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 all rooms
|
||||||
GET {{host}}/api/v1/rooms
|
GET {{host}}/api/v1/rooms
|
||||||
Authorization: Basic {{username}} {{password}}
|
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}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"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": {
|
"/rooms": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|||||||
@@ -9,6 +9,47 @@
|
|||||||
"host": "api.db.e.kb28.ch",
|
"host": "api.db.e.kb28.ch",
|
||||||
"basePath": "/api/v1",
|
"basePath": "/api/v1",
|
||||||
"paths": {
|
"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": {
|
"/rooms": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|||||||
@@ -6,6 +6,32 @@ info:
|
|||||||
title: Gateway API
|
title: Gateway API
|
||||||
version: "1.0"
|
version: "1.0"
|
||||||
paths:
|
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:
|
/rooms:
|
||||||
get:
|
get:
|
||||||
description: Get a list of all unique rooms from the measurement
|
description: Get a list of all unique rooms from the measurement
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ func (c *MappingConfig) NodesForRoom(room string) []string {
|
|||||||
return nodes
|
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.
|
// Rooms returns the list of all room names defined in the mapping.
|
||||||
func (c *MappingConfig) Rooms() []string {
|
func (c *MappingConfig) Rooms() []string {
|
||||||
seen := make(map[string]struct{})
|
seen := make(map[string]struct{})
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import (
|
|||||||
type RoomMapper interface {
|
type RoomMapper interface {
|
||||||
Rooms() []string
|
Rooms() []string
|
||||||
NodesForRoom(room string) []string
|
NodesForRoom(room string) []string
|
||||||
|
AllNodes() []string
|
||||||
|
GetRoom(nodeID string) (string, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestGateway struct {
|
type RestGateway struct {
|
||||||
@@ -71,6 +73,7 @@ func (g *RestGateway) setupRoutes() {
|
|||||||
v1.GET("/rooms", g.getRooms)
|
v1.GET("/rooms", g.getRooms)
|
||||||
v1.GET("/rooms/:room-id/current", g.getRoomCurrent)
|
v1.GET("/rooms/:room-id/current", g.getRoomCurrent)
|
||||||
v1.GET("/rooms/:room-id/history", g.getRoomHistory)
|
v1.GET("/rooms/:room-id/history", g.getRoomHistory)
|
||||||
|
v1.GET("/battery", g.getBattery)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
g.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
@@ -85,6 +88,77 @@ func buildNodeFilter(nodes []string) string {
|
|||||||
return "(" + strings.Join(parts, " OR ") + ")"
|
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 {
|
func (g *RestGateway) Run(addr string) error {
|
||||||
return g.engine.Run(addr)
|
return g.engine.Run(addr)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user