Compare commits
92 Commits
models-v1.
...
fa9c852b8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
fa9c852b8d
|
|||
|
8e85c5e20d
|
|||
|
4bce5e5ee2
|
|||
|
26f1553e26
|
|||
|
5902a47605
|
|||
|
09ee22ff50
|
|||
|
964a2d3375
|
|||
|
5ca64315eb
|
|||
|
9b06b9f6f7
|
|||
|
2ec7eefaa0
|
|||
|
3293d9194c
|
|||
|
|
d593dfd759
|
||
|
|
83705f4b5e
|
||
|
|
dffdcd36db
|
||
|
|
676913e280
|
||
|
|
8a559024a6
|
||
|
|
7c19b5dadb
|
||
|
|
c4e1315478
|
||
|
|
6b12612580
|
||
|
|
c32728ccbe
|
||
|
|
78167ceb38
|
||
|
|
3cdfbef680
|
||
|
|
94e0518fa6
|
||
|
|
f4ac6e91b0
|
||
|
|
6fd9959329
|
||
|
|
35fbe7c488
|
||
|
|
158767deec
|
||
|
|
bce2417ded
|
||
|
|
53ccda306b
|
||
|
|
71ed4c4bf9
|
||
|
|
e1d274470b
|
||
|
|
f0bc4ee373 | ||
|
|
f71db42f47 | ||
|
|
8b9e215cc6 | ||
|
|
caee92c595 | ||
|
2f7c88b701
|
|||
|
0f30749534
|
|||
|
f4ab3093c3
|
|||
|
89023b86ac
|
|||
|
811641c58b
|
|||
|
be5772e488
|
|||
|
53fbc87af6
|
|||
|
2b766d3d96
|
|||
|
7c000f3b9c
|
|||
|
5a3f8c3c5c
|
|||
|
2f57e886c0
|
|||
|
fab79aa6b6
|
|||
|
3ff484359e
|
|||
|
5198784c37
|
|||
|
eecb4a196b
|
|||
|
8c3b00edd8
|
|||
|
25c8327662
|
|||
|
2355c8f0e9
|
|||
|
c78f1e1509
|
|||
|
9776695228
|
|||
|
bde8184ad1
|
|||
|
022fb97153
|
|||
|
c34ec94a6b
|
|||
|
679f6fece2
|
|||
|
ef51f9b3ed
|
|||
|
04c3883744
|
|||
|
1bcaad895d
|
|||
|
56c6299417
|
|||
|
abf19fb4e2
|
|||
|
641f6af1f0
|
|||
|
ccec4efca6
|
|||
|
77574e1dfa
|
|||
|
c472064451
|
|||
|
c4cf1ba704
|
|||
|
980b43e669
|
|||
|
1678ac535b
|
|||
|
b8ddfefc2c
|
|||
|
50583bb79b
|
|||
|
790961b15a
|
|||
|
b4110b81eb
|
|||
|
bf7d0a7005
|
|||
|
fcdb5b5485
|
|||
|
9163fd494b
|
|||
|
c2a67684ed
|
|||
|
25438f085e
|
|||
|
cea4435bbc
|
|||
|
a49c3a8472
|
|||
|
2e8f92888e
|
|||
|
3ed1e36c56
|
|||
|
3587e10671
|
|||
|
0086a31f73
|
|||
|
a11573609e
|
|||
|
979c502e27
|
|||
|
1fb294f495
|
|||
|
567e9162e2
|
|||
|
5a1bfefffb
|
|||
|
28a8377231
|
2
.github/CODEOWNERS
vendored
@@ -2,4 +2,4 @@
|
||||
/gateway @DjeAvd
|
||||
/db/ @Klagarge
|
||||
/ui/ @khalil-bot
|
||||
/ml @imfeldd
|
||||
/model @AlisonLec
|
||||
|
||||
9
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: swag-init
|
||||
name: swag init
|
||||
entry: bash -c 'export PATH=$PATH:$(go env GOPATH)/bin && cd db/src && swag init'
|
||||
language: system
|
||||
files: ^db/src/.*\.go$
|
||||
pass_filenames: false
|
||||
2
GULAG.md
@@ -2,7 +2,7 @@
|
||||
|
||||
## General
|
||||
- Git history **has** to be kept linear. Branches **have** to be __rebased__ before being merged.
|
||||
- Merge commit is stricly forbidden.
|
||||
- Merge commit is strictly forbidden.
|
||||
- Force pushes are disallowed on shared branches unless discussed beforehand with affected people.
|
||||
|
||||
## Branches
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
INFLUX_PORT=8181
|
||||
UI_PORT=8093
|
||||
INFLUX_DATABASE=provence
|
||||
|
||||
REST_USERNAME=
|
||||
REST_PASSWORD=
|
||||
|
||||
MQTT_BROKER_URL=tls://mqtt.e.kb28.ch:8883
|
||||
MQTT_USERNAME=
|
||||
MQTT_PASSWORD=
|
||||
|
||||
28
db/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
### Go template
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
mapping.json
|
||||
26
db/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./src/go.mod ./src/go.sum ./
|
||||
RUN go mod tidy
|
||||
|
||||
COPY ./src .
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /gateway .
|
||||
|
||||
|
||||
FROM alpine:latest AS certs
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
FROM scratch AS final
|
||||
LABEL org.opencontainers.image.authors="remi.heredero@hevs.ch" \
|
||||
org.opencontainers.image.title="PI-E2EEDA Gateway MQTT/Influx/REST" \
|
||||
org.opencontainers.image.description="This container is an application for the E2EEDA PI. Use MQTT to communicate with devices, enabling real-time updates and control. Time-series data, such as device states, is stored in InfluxDB for analytics and monitoring. A REST API provides external access for managing devices and retrieving data." \
|
||||
org.opencontainers.image.source="https://github.com/PI-E2EEDA/Plein-de-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-project"
|
||||
|
||||
COPY --from=builder /gateway /gateway
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/gateway"]
|
||||
21
db/README.md
@@ -1,4 +1,4 @@
|
||||
# Deployement
|
||||
# Deployment
|
||||
|
||||
1. Create the Influx token offline:
|
||||
|
||||
@@ -18,5 +18,24 @@ cp .env.template .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Swagger Documentation
|
||||
To ensure the Swagger documentation is always up to date, we use [pre-commit](https://pre-commit.com/). It runs `swag init` automatically before each commit if any Go files in `db/src` have changed.
|
||||
|
||||
To install the hooks:
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
Alternatively, you can run it manually:
|
||||
```bash
|
||||
pre-commit run --all-files
|
||||
```
|
||||
or from `db/src`:
|
||||
```bash
|
||||
swag init
|
||||
```
|
||||
|
||||
## Traefik
|
||||
A traefik config file is available on [traefik.yml](./traefik.yml). It can be used with an OIDC provider ([Authentik](https://github.com/goauthentik/authentik) in our case) to control access to the database explorer.
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
--object-store=file \
|
||||
--data-dir=/var/lib/influxdb3/data \
|
||||
--admin-token-file=/tmp/admin-token.json \
|
||||
--query-file-limit=1000 \
|
||||
--disable-authz=health
|
||||
'
|
||||
secrets:
|
||||
@@ -63,6 +64,7 @@ services:
|
||||
mqtt:
|
||||
image: rabbitmq:4-management-alpine
|
||||
container_name: mqtt
|
||||
hostname: "mqtt"
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "15672:15672" # Management plugin HTTP port
|
||||
@@ -74,6 +76,36 @@ services:
|
||||
- RABBITMQ_DEFAULT_PASS=${MQTT_PASSWORD:?MQTT_PASSWORD is required}
|
||||
command: sh -c "rabbitmq-plugins enable rabbitmq_mqtt && rabbitmq-server"
|
||||
|
||||
gateway:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
depends_on:
|
||||
influxdb:
|
||||
condition: service_healthy
|
||||
mqtt:
|
||||
condition: service_started
|
||||
environment:
|
||||
- MQTT_BROKER_URL=${MQTT_BROKER_URL:-mqtt://mqtt:1883}
|
||||
- MQTT_USERNAME=${MQTT_USERNAME:?MQTT_USERNAME is required}
|
||||
- MQTT_PASSWORD=${MQTT_PASSWORD:?MQTT_PASSWORD is required}
|
||||
- INFLUX_URL=${INFLUX_URL:-http://influxdb:${INFLUX_PORT:-8181}}
|
||||
- INFLUX_DATABASE=${INFLUX_DATABASE}
|
||||
- INFLUX_TOKEN_FILE=/run/secrets/admin-token
|
||||
- REST_PORT=${REST_PORT:-8080}
|
||||
- REST_USERNAME=${REST_USERNAME}
|
||||
- REST_PASSWORD=${REST_PASSWORD}
|
||||
- MAPPING_CONFIG_PATH=/config/mapping.json
|
||||
- CO2_THREASHOLD_MAX=1400
|
||||
- CO2_THREASHOLD_MIN=1000
|
||||
secrets:
|
||||
- admin-token
|
||||
volumes:
|
||||
- ./mapping.json:/config/mapping.json:ro
|
||||
ports:
|
||||
- "${REST_PORT:-8080}:8080"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
influxdb3_data:
|
||||
rabbitmq_data:
|
||||
|
||||
49
db/export-csv-loop.http
Normal file
@@ -0,0 +1,49 @@
|
||||
@host = localhost:8080
|
||||
@username = user
|
||||
@password = password
|
||||
@from = 2026-05-27T14:00:00+02:00
|
||||
@to = 2026-05-27T18:00:00+02:00
|
||||
|
||||
### Export CSV - Room A2 - loop over all nodes
|
||||
< {%
|
||||
request.variables.set("nodesA2", [
|
||||
{"room": "A2", "mac": "E1:C0:30:15:4E:89"},
|
||||
{"room": "A2", "mac": "C6:7E:0A:DE:DA:74"},
|
||||
{"room": "A2", "mac": "E8:F3:0A:F7:3B:F3"},
|
||||
{"room": "A2", "mac": "C2:64:0F:68:35:3E"},
|
||||
{"room": "A2", "mac": "F5:80:05:76:53:F0"},
|
||||
{"room": "A2", "mac": "C6:95:1B:A6:49:E6"}
|
||||
])
|
||||
%}
|
||||
|
||||
GET {{host}}/api/v1/export/csv?node={{$.nodesA2..mac}}&from={{from}}&to={{to}}
|
||||
Authorization: Basic {{username}} {{password}}
|
||||
|
||||
> {%
|
||||
let current = request.variables.get("nodesA2")[request.iteration()]
|
||||
client.test(`Export CSV - Room ${current.room} - Node ${current.mac}`, () => {
|
||||
client.assert(response.status === 200, `Expected 200, got ${response.status}`)
|
||||
})
|
||||
%}
|
||||
|
||||
### Export CSV - Room A3 - loop over all nodes
|
||||
< {%
|
||||
request.variables.set("nodesA3", [
|
||||
{"room": "A3", "mac": "ED:B2:F3:74:3E:C2"},
|
||||
{"room": "A3", "mac": "CE:25:63:38:34:05"},
|
||||
{"room": "A3", "mac": "E6:8A:79:C8:87:25"},
|
||||
{"room": "A3", "mac": "DC:06:D9:40:7A:CB"},
|
||||
{"room": "A3", "mac": "D5:2F:7E:30:10:5A"},
|
||||
{"room": "A3", "mac": "EA:1A:AD:15:5E:9F"}
|
||||
])
|
||||
%}
|
||||
|
||||
GET {{host}}/api/v1/export/csv?node={{$.nodesA3..mac}}&from={{from}}&to={{to}}
|
||||
Authorization: Basic {{username}} {{password}}
|
||||
|
||||
> {%
|
||||
let current = request.variables.get("nodesA3")[request.iteration()]
|
||||
client.test(`Export CSV - Room ${current.room} - Node ${current.mac}`, () => {
|
||||
client.assert(response.status === 200, `Expected 200, got ${response.status}`)
|
||||
})
|
||||
%}
|
||||
31
db/get-db.http
Normal file
@@ -0,0 +1,31 @@
|
||||
@host = https://api.db.e.kb28.ch
|
||||
@room-id = B3
|
||||
@username = PIE2EEDA
|
||||
@password =
|
||||
|
||||
### GET last value of temp, co2, humidity, windows states
|
||||
GET {{host}}/api/v1/rooms/{{room-id}}/current
|
||||
Authorization: Basic {{username}} {{password}}
|
||||
|
||||
### GET history of a room
|
||||
@window = 1 day
|
||||
GET {{host}}/api/v1/rooms/{{room-id}}/history?window={{window}}
|
||||
Authorization: Basic {{username}} {{password}}
|
||||
|
||||
### GET CO2 status of all rooms
|
||||
GET {{host}}/api/v1/rooms/high-co2
|
||||
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
|
||||
|
||||
### Export sensor data as CSV for a node over a time range
|
||||
@node = C2:64:0F:68:35:3E
|
||||
@from = 2026-05-27T14:00:00+02:00
|
||||
@to = 2026-05-27T18:00:00+02:00
|
||||
GET {{host}}/api/v1/export/csv?node={{node}}&from={{from}}&to={{to}}
|
||||
Authorization: Basic {{username}} {{password}}
|
||||
8
db/mapping.json.template
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"campus": {
|
||||
"provence": ["gw-01"]
|
||||
},
|
||||
"room": {
|
||||
"B3": ["E1:C0:30:15:4E:89", "F9:CE:0C:A4:7C:A4"]
|
||||
}
|
||||
}
|
||||
337
db/src/docs/docs.go
Normal file
@@ -0,0 +1,337 @@
|
||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/export/csv": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Export CO2, temperature, humidity, window status and battery data for a node over a time range",
|
||||
"produces": [
|
||||
"text/csv"
|
||||
],
|
||||
"tags": [
|
||||
"export"
|
||||
],
|
||||
"summary": "Export sensor data as CSV",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node MAC address (e.g. E8:F3:0A:F7:3B:F3)",
|
||||
"name": "node",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Start time in RFC3339 format (e.g. 2026-05-27T13:00:00Z)",
|
||||
"name": "from",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "End time in RFC3339 format (e.g. 2026-05-27T15:00:00Z)",
|
||||
"name": "to",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "CSV file",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get a list of all unique rooms from the measurement",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get all unique rooms",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms/high-co2": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get a list of rooms where CO2 levels are above the threshold",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get rooms with high CO2 levels",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/rest.RoomCO2Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms/{room-id}/current": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get the latest record for a specific room",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get current data for a room",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Room ID",
|
||||
"name": "room-id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms/{room-id}/history": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get history for a specific room",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get history for a room",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Room ID",
|
||||
"name": "room-id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"default": "1 day",
|
||||
"description": "Time window (e.g., 1 day, 1 hour, 30 min)",
|
||||
"name": "window",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"rest.RoomCO2Status": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"co2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_high": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"room": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BasicAuth": {
|
||||
"type": "basic"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "api.db.e.kb28.ch",
|
||||
BasePath: "/api/v1",
|
||||
Schemes: []string{},
|
||||
Title: "Gateway API",
|
||||
Description: "This is a gateway API for IoT data.",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
313
db/src/docs/swagger.json
Normal file
@@ -0,0 +1,313 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is a gateway API for IoT data.",
|
||||
"title": "Gateway API",
|
||||
"contact": {},
|
||||
"version": "1.0"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/export/csv": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Export CO2, temperature, humidity, window status and battery data for a node over a time range",
|
||||
"produces": [
|
||||
"text/csv"
|
||||
],
|
||||
"tags": [
|
||||
"export"
|
||||
],
|
||||
"summary": "Export sensor data as CSV",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Node MAC address (e.g. E8:F3:0A:F7:3B:F3)",
|
||||
"name": "node",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Start time in RFC3339 format (e.g. 2026-05-27T13:00:00Z)",
|
||||
"name": "from",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "End time in RFC3339 format (e.g. 2026-05-27T15:00:00Z)",
|
||||
"name": "to",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "CSV file",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get a list of all unique rooms from the measurement",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get all unique rooms",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms/high-co2": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get a list of rooms where CO2 levels are above the threshold",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get rooms with high CO2 levels",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/rest.RoomCO2Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms/{room-id}/current": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get the latest record for a specific room",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get current data for a room",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Room ID",
|
||||
"name": "room-id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/rooms/{room-id}/history": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get history for a specific room",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"rooms"
|
||||
],
|
||||
"summary": "Get history for a room",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Room ID",
|
||||
"name": "room-id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"default": "1 day",
|
||||
"description": "Time window (e.g., 1 day, 1 hour, 30 min)",
|
||||
"name": "window",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"rest.RoomCO2Status": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"co2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_high": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"room": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"BasicAuth": {
|
||||
"type": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
201
db/src/docs/swagger.yaml
Normal file
@@ -0,0 +1,201 @@
|
||||
basePath: /api/v1
|
||||
definitions:
|
||||
rest.RoomCO2Status:
|
||||
properties:
|
||||
co2:
|
||||
type: integer
|
||||
is_high:
|
||||
type: boolean
|
||||
room:
|
||||
type: string
|
||||
type: object
|
||||
host: api.db.e.kb28.ch
|
||||
info:
|
||||
contact: {}
|
||||
description: This is a gateway API for IoT data.
|
||||
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
|
||||
/export/csv:
|
||||
get:
|
||||
description: Export CO2, temperature, humidity, window status and battery data
|
||||
for a node over a time range
|
||||
parameters:
|
||||
- description: Node MAC address (e.g. E8:F3:0A:F7:3B:F3)
|
||||
in: query
|
||||
name: node
|
||||
required: true
|
||||
type: string
|
||||
- description: Start time in RFC3339 format (e.g. 2026-05-27T13:00:00Z)
|
||||
in: query
|
||||
name: from
|
||||
required: true
|
||||
type: string
|
||||
- description: End time in RFC3339 format (e.g. 2026-05-27T15:00:00Z)
|
||||
in: query
|
||||
name: to
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- text/csv
|
||||
responses:
|
||||
"200":
|
||||
description: CSV file
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Export sensor data as CSV
|
||||
tags:
|
||||
- export
|
||||
/rooms:
|
||||
get:
|
||||
description: Get a list of all unique rooms from the measurement
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Get all unique rooms
|
||||
tags:
|
||||
- rooms
|
||||
/rooms/{room-id}/current:
|
||||
get:
|
||||
description: Get the latest record for a specific room
|
||||
parameters:
|
||||
- description: Room ID
|
||||
in: path
|
||||
name: room-id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Get current data for a room
|
||||
tags:
|
||||
- rooms
|
||||
/rooms/{room-id}/history:
|
||||
get:
|
||||
description: Get history for a specific room
|
||||
parameters:
|
||||
- description: Room ID
|
||||
in: path
|
||||
name: room-id
|
||||
required: true
|
||||
type: string
|
||||
- default: 1 day
|
||||
description: Time window (e.g., 1 day, 1 hour, 30 min)
|
||||
in: query
|
||||
name: window
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Get history for a room
|
||||
tags:
|
||||
- rooms
|
||||
/rooms/high-co2:
|
||||
get:
|
||||
description: Get a list of rooms where CO2 levels are above the threshold
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/rest.RoomCO2Status'
|
||||
type: array
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: Get rooms with high CO2 levels
|
||||
tags:
|
||||
- rooms
|
||||
securityDefinitions:
|
||||
BasicAuth:
|
||||
type: basic
|
||||
swagger: "2.0"
|
||||
71
db/src/go.mod
Normal file
@@ -0,0 +1,71 @@
|
||||
module gateway
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/InfluxCommunity/influxdb3-go/v2 v2.13.0
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/apache/arrow-go/v18 v18.5.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/influxdata/line-protocol/v2 v2.2.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.23 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
258
db/src/go.sum
Normal file
@@ -0,0 +1,258 @@
|
||||
github.com/InfluxCommunity/influxdb3-go/v2 v2.13.0 h1:IQVpiJ0t92OsJXf/RJ0+HHoIbK3mgJaMntEfLRgxS9Q=
|
||||
github.com/InfluxCommunity/influxdb3-go/v2 v2.13.0/go.mod h1:fXhSEgDgX7iv++4t5cFVKRB6kqqeLjRRf5d2IlUdiWw=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apache/arrow-go/v18 v18.5.1 h1:yaQ6zxMGgf9YCYw4/oaeOU3AULySDlAYDOcnr4LdHdI=
|
||||
github.com/apache/arrow-go/v18 v18.5.1/go.mod h1:OCCJsmdq8AsRm8FkBSSmYTwL/s4zHW9CqxeBxEytkNE=
|
||||
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
|
||||
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||
github.com/frankban/quicktest v1.11.0/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
|
||||
github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
|
||||
github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=
|
||||
github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/influxdata/line-protocol-corpus v0.0.0-20210519164801-ca6fa5da0184/go.mod h1:03nmhxzZ7Xk2pdG+lmMd7mHDfeVOYFyhOgwO61qWU98=
|
||||
github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937 h1:MHJNQ+p99hFATQm6ORoLmpUCF7ovjwEFshs/NHzAbig=
|
||||
github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937/go.mod h1:BKR9c0uHSmRgM/se9JhFHtTT7JTO67X23MtKMHtZcpo=
|
||||
github.com/influxdata/line-protocol/v2 v2.0.0-20210312151457-c52fdecb625a/go.mod h1:6+9Xt5Sq1rWx+glMgxhcg2c0DUaehK+5TDcPZ76GypY=
|
||||
github.com/influxdata/line-protocol/v2 v2.1.0/go.mod h1:QKw43hdUBg3GTk2iC3iyCxksNj7PX9aUSeYOYE/ceHY=
|
||||
github.com/influxdata/line-protocol/v2 v2.2.1 h1:EAPkqJ9Km4uAxtMRgUubJyqAr6zgWM0dznKMLRauQRE=
|
||||
github.com/influxdata/line-protocol/v2 v2.2.1/go.mod h1:DmB3Cnh+3oxmG6LOBIxce4oaL4CPj3OmMPgvauXh+tM=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
|
||||
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
|
||||
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo=
|
||||
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
96
db/src/influx/influx.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Package influx provides an abstraction to the client influx.
|
||||
package influx
|
||||
|
||||
import (
|
||||
"context"
|
||||
datapoint "gateway/point"
|
||||
|
||||
"github.com/InfluxCommunity/influxdb3-go/v2/influxdb3"
|
||||
"github.com/InfluxCommunity/influxdb3-go/v2/influxdb3/batching"
|
||||
)
|
||||
|
||||
const BatchSize = 50 // Number of points to batch before pushing to InfluxDB
|
||||
const BatchCapacity = 50 // Capacity maximum of the buffer
|
||||
|
||||
// Gateway provides the abstraction of all gateways to send data points
|
||||
type Gateway interface {
|
||||
AddDatapoint(dp datapoint.DataPointInfo) error
|
||||
Close() error
|
||||
Flush() error
|
||||
Query(ctx context.Context, query string) (*influxdb3.QueryIterator, error)
|
||||
}
|
||||
|
||||
// An InfluxGateway is the abstracted influx gateway.
|
||||
// It provides the influx parameters to initialize the connection
|
||||
// and the batcher to batch the points before pushing to Influx.
|
||||
type InfluxGateway struct {
|
||||
client *influxdb3.Client
|
||||
batcher *batching.Batcher
|
||||
}
|
||||
|
||||
// NewInfluxGateway creates a new InfluxGateway with the given parameters.
|
||||
func NewInfluxGateway(url string, token string, database string) (*InfluxGateway, error) {
|
||||
|
||||
client, err := influxdb3.New(influxdb3.ClientConfig{
|
||||
Host: url,
|
||||
Token: token,
|
||||
Database: database,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &InfluxGateway{
|
||||
client: client,
|
||||
batcher: batching.NewBatcher(
|
||||
batching.WithSize(BatchSize),
|
||||
batching.WithInitialCapacity(BatchCapacity),
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddDatapoint is used to add a datapoint in the batcher. It uses the
|
||||
// DataPointInfo interface for abstracting the generic type of the DataPoint.
|
||||
// It pushes the batch of point when the number of points >= batch size.
|
||||
func (g *InfluxGateway) AddDatapoint(dp datapoint.DataPointInfo) error {
|
||||
|
||||
tagsList := map[string]string{}
|
||||
for _, t := range dp.Tags() {
|
||||
tagsList[t.Subject] = t.Content
|
||||
}
|
||||
g.batcher.Add(
|
||||
influxdb3.NewPoint(
|
||||
dp.MeasurementName(),
|
||||
tagsList,
|
||||
dp.PayloadAsAny(),
|
||||
dp.Timestamp(),
|
||||
),
|
||||
)
|
||||
|
||||
// if not ready, we are done
|
||||
if g.batcher.Ready() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If ready, flush and return the result directly
|
||||
return g.Flush()
|
||||
}
|
||||
|
||||
// Flush sends the current batch of points to InfluxDB.
|
||||
func (g *InfluxGateway) Flush() error {
|
||||
// Send batch to influx DB
|
||||
err := g.client.WritePoints(context.Background(), g.batcher.Emit())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes the InfluxGateway client.
|
||||
func (g *InfluxGateway) Close() error {
|
||||
return g.client.Close()
|
||||
}
|
||||
|
||||
// Query executes a SQL query against InfluxDB using the Arrow Flight (gRPC) API.
|
||||
func (g *InfluxGateway) Query(ctx context.Context, query string) (*influxdb3.QueryIterator, error) {
|
||||
return g.client.Query(ctx, query)
|
||||
}
|
||||
195
db/src/main.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gateway/influx"
|
||||
"gateway/mqtt"
|
||||
point "gateway/point"
|
||||
"gateway/rest"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
type ProvenceData struct {
|
||||
CO2PPM int `json:"co2_ppm"`
|
||||
Temp float64 `json:"temp"`
|
||||
Humidity int `json:"humidity"`
|
||||
Battery int `json:"battery"`
|
||||
Window bool `json:"window_open"`
|
||||
}
|
||||
|
||||
func mqttConnection() *mqtt.MqttGateway {
|
||||
BrokerUrl := getEnv("MQTT_BROKER_URL", "tls://localhost:8883")
|
||||
Username := getEnv("MQTT_USERNAME", "user")
|
||||
Password := getEnv("MQTT_PASSWORD", "password")
|
||||
|
||||
ClientId := "mqtt-gateway-test-client_" + fmt.Sprint(time.Now().Unix())
|
||||
|
||||
// Create config & gateway
|
||||
mqttP := &mqtt.MqttParams{
|
||||
Broker: BrokerUrl,
|
||||
ClientId: ClientId,
|
||||
Qos: 1,
|
||||
Username: Username,
|
||||
Password: Password,
|
||||
TlsConfig: nil,
|
||||
OnConnect: nil,
|
||||
OnConnectionLost: nil,
|
||||
Timeout: 1 * time.Second,
|
||||
}
|
||||
gateway, err := mqtt.NewMqttGateway(*mqttP)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return gateway
|
||||
}
|
||||
|
||||
func influxConnection() *influx.InfluxGateway {
|
||||
influxUrl := getEnv("INFLUX_URL", "http://influxdb:8181")
|
||||
|
||||
influxDatabase := getEnv("INFLUX_DATABASE", "provence")
|
||||
influxToken := getEnv("INFLUX_TOKEN", "")
|
||||
if influxToken == "" {
|
||||
if tokenFile := getEnv("INFLUX_TOKEN_FILE", "/run/secrets/admin-token"); tokenFile != "" {
|
||||
content, err := os.ReadFile(tokenFile)
|
||||
if err == nil {
|
||||
influxToken = strings.TrimSpace(string(content))
|
||||
} else {
|
||||
log.Printf("[Main] Warning: could not read token file %s: %v\n", tokenFile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if influxToken == "" {
|
||||
influxToken = "password"
|
||||
}
|
||||
|
||||
log.Printf("[Main] InfluxDB config: URL=%s, DB=%s\n", influxUrl, influxDatabase)
|
||||
|
||||
// Create the gateway
|
||||
gateway, err := influx.NewInfluxGateway(influxUrl, influxToken, influxDatabase)
|
||||
if err != nil {
|
||||
log.Fatalf("Creating gateway failed !, %v", err)
|
||||
}
|
||||
|
||||
return gateway
|
||||
}
|
||||
|
||||
// @title Gateway API
|
||||
// @version 1.0
|
||||
// @description This is a gateway API for IoT data.
|
||||
// @host api.db.e.kb28.ch
|
||||
// @BasePath /api/v1
|
||||
// @securityDefinitions.basic BasicAuth
|
||||
func main() {
|
||||
noMqtt := slices.Contains(os.Args[1:], "--no-mqtt")
|
||||
|
||||
// Load mapping configuration (reloaded dynamically on each access)
|
||||
mappingPath := getEnv("MAPPING_CONFIG_PATH", "mapping.json")
|
||||
mapping := NewDynamicMapping(mappingPath)
|
||||
|
||||
influxGateway := influxConnection()
|
||||
defer influxGateway.Close()
|
||||
|
||||
measurementName := getEnv("CAMPUS", "provence")
|
||||
|
||||
if !noMqtt {
|
||||
mqttGateway := mqttConnection()
|
||||
defer mqttGateway.Disconnect()
|
||||
|
||||
// Create measurement for provence topic
|
||||
provenceMeasurement := point.CreateMeasurement[ProvenceData](measurementName)
|
||||
// The incoming MQTT topic structure is: <gateway_id>/<node_id>/update
|
||||
topicStructur := []string{"gateway", "node"}
|
||||
err := mqtt.SubscribeTyped(mqttGateway, "+/+/update", provenceMeasurement, topicStructur, func(dp point.DataPoint[ProvenceData]) {
|
||||
var gatewayID, nodeID string
|
||||
for _, tag := range dp.Tags() {
|
||||
switch tag.Subject {
|
||||
case "gateway":
|
||||
gatewayID = tag.Content
|
||||
case "node":
|
||||
nodeID = tag.Content
|
||||
}
|
||||
}
|
||||
|
||||
campus, campusOk := mapping.GetCampus(gatewayID)
|
||||
room, roomOk := mapping.GetRoom(nodeID)
|
||||
|
||||
batteryLevel := dp.GetValues().Battery
|
||||
log.Printf("[Main] Received gateway=%s node=%s -> campus=%s room=%s (Battery %d%%)\n", gatewayID, nodeID, campus, room, batteryLevel)
|
||||
|
||||
var influxTags []point.Topic
|
||||
|
||||
if !campusOk {
|
||||
log.Printf("[Main] No mapping found for gateway=%s\n", gatewayID)
|
||||
influxTags = []point.Topic{
|
||||
{Subject: "node", Content: nodeID},
|
||||
}
|
||||
} else if !roomOk {
|
||||
log.Printf("[Main] No mapping found for gateway=%s\n", gatewayID)
|
||||
influxTags = []point.Topic{
|
||||
{Subject: "campus", Content: campus},
|
||||
{Subject: "node", Content: nodeID},
|
||||
}
|
||||
} else {
|
||||
influxTags = []point.Topic{
|
||||
{Subject: "campus", Content: campus},
|
||||
{Subject: "room", Content: room},
|
||||
{Subject: "node", Content: nodeID},
|
||||
}
|
||||
}
|
||||
|
||||
// If CO2PPM value is present and over 1,000,000,000 delete the field, it's calibration value
|
||||
values := dp.GetValues()
|
||||
if values.CO2PPM > 1000000000 {
|
||||
log.Printf("[Main] Warning: CO2PPM value %d is calibrating, setting to 0\n", values.CO2PPM)
|
||||
values.CO2PPM = 0
|
||||
}
|
||||
|
||||
// Still too high value, something wrong, dropping datapoint
|
||||
if values.CO2PPM > 10000 {
|
||||
log.Printf("[Main] Error: CO2PPM value %d is over threshold, dropping Datapoint\n", values.CO2PPM)
|
||||
return
|
||||
}
|
||||
|
||||
translatedDp := provenceMeasurement.CreateDataPoint(influxTags, values, dp.Timestamp())
|
||||
|
||||
if err := influxGateway.AddDatapoint(&translatedDp); err != nil {
|
||||
log.Printf("[Main] Error adding datapoint to influx: %v\n", err)
|
||||
}
|
||||
if err := influxGateway.Flush(); err != nil {
|
||||
log.Printf("[Main] Error flushing to influx: %v\n", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Println("[Main] MQTT disabled (--no-mqtt flag set)")
|
||||
}
|
||||
|
||||
// Initialize and start REST Gateway
|
||||
restUsername := getEnv("REST_USERNAME", "user")
|
||||
restPassword := getEnv("REST_PASSWORD", "password")
|
||||
restGateway := rest.NewRestGateway(influxGateway, mapping, measurementName, restUsername, restPassword)
|
||||
|
||||
port := getEnv("REST_PORT", "8080")
|
||||
|
||||
log.Printf("[Main] Starting REST Gateway on port %s\n", port)
|
||||
if err := restGateway.Run(":" + port); err != nil {
|
||||
log.Fatalf("[Main] Failed to start REST Gateway: %v", err)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
152
db/src/mapping.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// mappingFile is the structure of the JSON config file.
|
||||
// Campus names map to the list of gateway IDs that belong to them.
|
||||
// Room names map to the list of node IDs that belong to them.
|
||||
type mappingFile struct {
|
||||
Campus map[string][]string `json:"campus"`
|
||||
Room map[string][]string `json:"room"`
|
||||
}
|
||||
|
||||
// MappingConfig holds the reverse lookup maps built from the config file:
|
||||
//
|
||||
// gateway_id -> campus name
|
||||
// node_id -> room name
|
||||
type MappingConfig struct {
|
||||
gatewayToCampus map[string]string
|
||||
nodeToRoom map[string]string
|
||||
}
|
||||
|
||||
// DynamicMapping reloads the mapping file on every access so that changes
|
||||
// to the JSON file take effect without restarting the programme.
|
||||
type DynamicMapping struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// NewDynamicMapping creates a DynamicMapping that reads from the given path.
|
||||
func NewDynamicMapping(path string) *DynamicMapping {
|
||||
return &DynamicMapping{path: path}
|
||||
}
|
||||
|
||||
func (d *DynamicMapping) load() *MappingConfig {
|
||||
cfg, err := LoadMapping(d.path)
|
||||
if err != nil {
|
||||
return EmptyMapping()
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (d *DynamicMapping) GetCampus(gatewayID string) (string, bool) {
|
||||
return d.load().GetCampus(gatewayID)
|
||||
}
|
||||
|
||||
func (d *DynamicMapping) GetRoom(nodeID string) (string, bool) {
|
||||
return d.load().GetRoom(nodeID)
|
||||
}
|
||||
|
||||
func (d *DynamicMapping) NodesForRoom(room string) []string {
|
||||
return d.load().NodesForRoom(room)
|
||||
}
|
||||
|
||||
func (d *DynamicMapping) AllNodes() []string {
|
||||
return d.load().AllNodes()
|
||||
}
|
||||
|
||||
func (d *DynamicMapping) Rooms() []string {
|
||||
return d.load().Rooms()
|
||||
}
|
||||
|
||||
// EmptyMapping returns a MappingConfig with no entries.
|
||||
// GetCampus and GetRoom will return their "unknown_*" fallback values.
|
||||
func EmptyMapping() *MappingConfig {
|
||||
return &MappingConfig{
|
||||
gatewayToCampus: make(map[string]string),
|
||||
nodeToRoom: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadMapping reads the mapping configuration from a JSON file and builds
|
||||
// the internal reverse-lookup tables.
|
||||
func LoadMapping(path string) (*MappingConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read mapping file %q: %w", path, err)
|
||||
}
|
||||
|
||||
var raw mappingFile
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse mapping JSON: %w", err)
|
||||
}
|
||||
|
||||
cfg := &MappingConfig{
|
||||
gatewayToCampus: make(map[string]string),
|
||||
nodeToRoom: make(map[string]string),
|
||||
}
|
||||
|
||||
for campus, gateways := range raw.Campus {
|
||||
for _, gwID := range gateways {
|
||||
cfg.gatewayToCampus[gwID] = campus
|
||||
}
|
||||
}
|
||||
|
||||
for room, nodes := range raw.Room {
|
||||
for _, nodeID := range nodes {
|
||||
cfg.nodeToRoom[nodeID] = room
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetCampus returns the campus name for a given gateway ID.
|
||||
// The boolean is false if the gateway ID has no mapping.
|
||||
func (c *MappingConfig) GetCampus(gatewayID string) (string, bool) {
|
||||
campus, ok := c.gatewayToCampus[gatewayID]
|
||||
return campus, ok
|
||||
}
|
||||
|
||||
// GetRoom returns the room name for a given node ID.
|
||||
// The boolean is false if the node ID has no mapping.
|
||||
func (c *MappingConfig) GetRoom(nodeID string) (string, bool) {
|
||||
room, ok := c.nodeToRoom[nodeID]
|
||||
return room, ok
|
||||
}
|
||||
|
||||
// NodesForRoom returns the list of node IDs that belong to the given room.
|
||||
func (c *MappingConfig) NodesForRoom(room string) []string {
|
||||
var nodes []string
|
||||
for nodeID, r := range c.nodeToRoom {
|
||||
if r == room {
|
||||
nodes = append(nodes, nodeID)
|
||||
}
|
||||
}
|
||||
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{})
|
||||
for _, room := range c.nodeToRoom {
|
||||
seen[room] = struct{}{}
|
||||
}
|
||||
rooms := make([]string, 0, len(seen))
|
||||
for room := range seen {
|
||||
rooms = append(rooms, room)
|
||||
}
|
||||
return rooms
|
||||
}
|
||||
248
db/src/mqtt/mqtt.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// Package mqtt_gateway provides an abstraction to an MQTT broker client.
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
dp "gateway/point"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
const (
|
||||
maxQoS = 2
|
||||
defaultTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// A MqttParams is the abstracted MQTT gateway.
|
||||
// It provides the MQTT parameters to initialize the connection and the method to add data.
|
||||
type MqttParams struct {
|
||||
Broker string
|
||||
ClientId string
|
||||
Qos byte
|
||||
Username string
|
||||
Password string
|
||||
TlsConfig *tls.Config
|
||||
OnConnect mqtt.OnConnectHandler
|
||||
OnConnectionLost mqtt.ConnectionLostHandler
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// MqttGateway is the abstracted MQTT gateway.
|
||||
// It connects to the MQTT broker and provides the method to send data to the broker.
|
||||
type MqttGateway struct {
|
||||
MqttParams MqttParams
|
||||
Client mqtt.Client
|
||||
}
|
||||
|
||||
// mqttPayload is the JSON structure published to the broker in a specific topic.
|
||||
// It contains the values and the timestamp of the data.
|
||||
type mqttPayload map[string]any
|
||||
|
||||
// connectHandler is called when the client connects to the broker. It prints a message to the console.
|
||||
var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) {
|
||||
log.Println("[MQTT Gateway] Connected to MQTT Broker")
|
||||
}
|
||||
|
||||
// connectLostHandler is called when the client loses connection to the broker. It prints a message to the console with the error.
|
||||
var connectLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {
|
||||
log.Printf("[MQTT Gateway] Connection lost: %v\n", err)
|
||||
}
|
||||
|
||||
func getTopic(t []dp.Topic) string {
|
||||
var topic []string
|
||||
for _, t := range t {
|
||||
topic = append(topic, t.Content)
|
||||
}
|
||||
return strings.Join(topic, "/")
|
||||
}
|
||||
|
||||
// NewMqttGateway creates a new MqttGateway with the given parameters.
|
||||
// And establishes the connection
|
||||
func NewMqttGateway(p MqttParams) (*MqttGateway, error) {
|
||||
// Verify input variable
|
||||
if p.Broker == "" {
|
||||
return nil, errors.New("[MQTT Gateway] Invalid broker address")
|
||||
}
|
||||
|
||||
if p.ClientId == "" {
|
||||
return nil, errors.New("[MQTT Gateway] Invalid client id")
|
||||
}
|
||||
|
||||
if p.Qos > maxQoS {
|
||||
return nil, errors.New("[MQTT Gateway] Invalid QoS level")
|
||||
}
|
||||
|
||||
if p.Timeout == 0 {
|
||||
// Set to default value
|
||||
p.Timeout = defaultTimeout
|
||||
}
|
||||
|
||||
opts := mqtt.NewClientOptions()
|
||||
opts.AddBroker(p.Broker)
|
||||
opts.SetClientID(p.ClientId)
|
||||
|
||||
if p.TlsConfig != nil {
|
||||
opts.SetTLSConfig(p.TlsConfig)
|
||||
}
|
||||
|
||||
if p.OnConnect != nil {
|
||||
opts.SetOnConnectHandler(p.OnConnect)
|
||||
} else {
|
||||
opts.SetOnConnectHandler(connectHandler)
|
||||
}
|
||||
|
||||
if p.OnConnectionLost != nil {
|
||||
opts.SetConnectionLostHandler(p.OnConnectionLost)
|
||||
} else {
|
||||
opts.SetConnectionLostHandler(connectLostHandler)
|
||||
}
|
||||
|
||||
if p.Username != "" {
|
||||
opts.SetUsername(p.Username)
|
||||
opts.SetPassword(p.Password)
|
||||
}
|
||||
|
||||
client := mqtt.NewClient(opts)
|
||||
token := client.Connect()
|
||||
if !token.WaitTimeout(p.Timeout) {
|
||||
return nil, fmt.Errorf("[MQTT Gateway] Mqtt connect timed out")
|
||||
}
|
||||
if err := token.Error(); err != nil {
|
||||
return nil, fmt.Errorf("[MQTT Gateway] Mqtt connect failed: %w", err)
|
||||
}
|
||||
|
||||
return &MqttGateway{
|
||||
MqttParams: p,
|
||||
Client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendData is used to send data in the MQTT gateway.
|
||||
// It uses the DataPointInfo interface for abstracting the generic type of the DataPoint
|
||||
func (g *MqttGateway) SendData(msg dp.DataPointInfo) error {
|
||||
|
||||
topic := getTopic(msg.Tags())
|
||||
if topic == "" {
|
||||
return errors.New("[MQTT Gateway] Invalid topic")
|
||||
}
|
||||
|
||||
payload := mqttPayload{
|
||||
"timestamp": msg.Timestamp().Unix(),
|
||||
}
|
||||
|
||||
for key, value := range msg.PayloadAsAny() {
|
||||
payload[key] = value
|
||||
}
|
||||
|
||||
payloadJson, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[MQTT Gateway] Failed to marshal payload: %w", err)
|
||||
}
|
||||
|
||||
token := g.Client.Publish(topic, g.MqttParams.Qos, false, payloadJson)
|
||||
if !token.WaitTimeout(g.MqttParams.Timeout) {
|
||||
return fmt.Errorf("[MQTT Gateway] Mqtt connect timed out")
|
||||
}
|
||||
if token.Error() != nil {
|
||||
return fmt.Errorf("[MQTT Gateway] Failed to publish message: %w", token.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect is used to disconnect the MQTT gateway from the broker.
|
||||
// It prints a message to the console when the disconnection is successful.
|
||||
func (g *MqttGateway) Disconnect() {
|
||||
g.Client.Disconnect(0)
|
||||
log.Println("[MQTT Gateway] Disconnected from MQTT Broker")
|
||||
}
|
||||
|
||||
// Subscribe is used to subscribe to a topic in the MQTT gateway.
|
||||
// It takes a topic and a callback function as parameters.
|
||||
// The callback function is called when a message is received on the subscribed topic.
|
||||
func (g *MqttGateway) Subscribe(topic string, callback mqtt.MessageHandler) error {
|
||||
token := g.Client.Subscribe(topic, g.MqttParams.Qos, callback)
|
||||
if !token.WaitTimeout(g.MqttParams.Timeout) {
|
||||
return fmt.Errorf("[MQTT Gateway] MQTT gateway timed out")
|
||||
}
|
||||
if token.Error() != nil {
|
||||
return fmt.Errorf("[MQTT Gateway] MQTT gateway failed to subscribe: %w", token.Error())
|
||||
}
|
||||
|
||||
log.Printf("[MQTT Gateway] Subscribed to topic: %s\n", topic)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unsubscribe is used to unsubscribe from a topic in the MQTT gateway.
|
||||
func (g *MqttGateway) Unsubscribe(topic string) error {
|
||||
token := g.Client.Unsubscribe(topic)
|
||||
if !token.WaitTimeout(g.MqttParams.Timeout) {
|
||||
return fmt.Errorf("[MQTT Gateway] MQTT gateway timed out")
|
||||
}
|
||||
if token.Error() != nil {
|
||||
return fmt.Errorf("[MQTT Gateway] MQTT gateway failed to unsubscribe: %w", token.Error())
|
||||
}
|
||||
|
||||
log.Printf("[MQTT Gateway] Unsubscribed from topic: %s\n", topic)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubscribeTyped is a helper to subscribe to a topic and automatically convert
|
||||
// the received JSON message to a DataPoint of type T.
|
||||
// T should be a struct or a map that matches the JSON payload (excluding timestamp).
|
||||
// tagSubjects is a list of tag subjects that correspond to the parts of the topic.
|
||||
// For example, if the topic is "provence/B3/update" and tagSubjects is ["city", "room"],
|
||||
// it will create tags {Subject: "city", Content: "provence"} and {Subject: "room", Content: "B3"}.
|
||||
func SubscribeTyped[T any](g *MqttGateway, topic string, m dp.Measurement[T], tagSubjects []string, handler func(dp.DataPoint[T])) error {
|
||||
return g.Subscribe(topic, func(client mqtt.Client, msg mqtt.Message) {
|
||||
// Unmarshal into T for fields
|
||||
var fields T
|
||||
if err := json.Unmarshal(msg.Payload(), &fields); err != nil {
|
||||
log.Printf("[MQTT Gateway] Error unmarshaling fields: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal into a map to extract timestamp
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(msg.Payload(), &raw); err != nil {
|
||||
log.Printf("[MQTT Gateway] Error unmarshaling raw: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ts := time.Now()
|
||||
if tsRaw, ok := raw["timestamp"].(string); ok {
|
||||
// Try RFC3339 first (default)
|
||||
if parsedTs, err := time.Parse(time.RFC3339, tsRaw); err == nil {
|
||||
ts = parsedTs
|
||||
} else {
|
||||
log.Printf("[MQTT Gateway] Failed to parse timestamp '%s' as RFC3339: %v", tsRaw, err)
|
||||
}
|
||||
} else if tsRaw, ok := raw["timestamp"].(float64); ok {
|
||||
// Handle Unix timestamp in seconds
|
||||
ts = time.Unix(int64(tsRaw), 0)
|
||||
}
|
||||
|
||||
// Extract tags from topic
|
||||
parts := strings.Split(msg.Topic(), "/")
|
||||
var tags []dp.Topic
|
||||
for i, subject := range tagSubjects {
|
||||
if i < len(parts) && subject != "" {
|
||||
tags = append(tags, dp.Topic{Subject: subject, Content: parts[i]})
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for backward compatibility if no tagSubjects provided
|
||||
if len(tagSubjects) == 0 && len(parts) > 1 {
|
||||
tags = append(tags, dp.Topic{Subject: "id", Content: parts[1]})
|
||||
}
|
||||
|
||||
dp := m.CreateDataPoint(tags, fields, ts)
|
||||
handler(dp)
|
||||
})
|
||||
}
|
||||
82
db/src/point/datapoint.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Package datapoint implements measurement and datapoint
|
||||
// for database gateways.
|
||||
package datapoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Topic struct {
|
||||
Subject string
|
||||
Content string
|
||||
}
|
||||
|
||||
// DataPointInfo provides an interface for accessing
|
||||
// all fields of a DataPoint.
|
||||
type DataPointInfo interface {
|
||||
MeasurementName() string
|
||||
Tags() []Topic
|
||||
PayloadAsAny() map[string]any
|
||||
Timestamp() time.Time
|
||||
}
|
||||
|
||||
// A Measurement represents a type of measurement such as
|
||||
// temperature, humidity, ...
|
||||
type Measurement[T any] struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// A DataPoint represents values associated with a measurement, along with tags,
|
||||
// values, and a timestamp. It contains a pointer to its parent Measurement
|
||||
type DataPoint[T any] struct {
|
||||
measurement *Measurement[T]
|
||||
tags []Topic
|
||||
values T
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func CreateMeasurement[T any](name string) Measurement[T] {
|
||||
return Measurement[T]{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDataPoint forces the creation of DataPoint with the type according
|
||||
// to the generic.
|
||||
func (m *Measurement[T]) CreateDataPoint(tags []Topic, values T, timestamp time.Time) DataPoint[T] {
|
||||
return DataPoint[T]{
|
||||
measurement: m,
|
||||
tags: tags,
|
||||
values: values,
|
||||
timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
func (dp *DataPoint[T]) MeasurementName() string {
|
||||
return dp.measurement.name
|
||||
}
|
||||
|
||||
func (dp *DataPoint[T]) Tags() []Topic {
|
||||
return dp.tags
|
||||
}
|
||||
|
||||
func (dp *DataPoint[T]) GetValues() T {
|
||||
return dp.values
|
||||
}
|
||||
|
||||
// PayloadAsAny returns the DataPoint value with type any.
|
||||
// (useful for adding the DataPoint for batching)
|
||||
func (dp *DataPoint[T]) PayloadAsAny() map[string]any {
|
||||
b, err := json.Marshal(dp.values)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var res map[string]any
|
||||
_ = json.Unmarshal(b, &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func (dp *DataPoint[T]) Timestamp() time.Time {
|
||||
return dp.timestamp
|
||||
}
|
||||
598
db/src/rest/rest.go
Normal file
@@ -0,0 +1,598 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
_ "gateway/docs"
|
||||
"gateway/influx"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apache/arrow-go/v18/arrow"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// RoomMapper is an interface for mapping node IDs to rooms and listing rooms.
|
||||
type RoomMapper interface {
|
||||
Rooms() []string
|
||||
NodesForRoom(room string) []string
|
||||
AllNodes() []string
|
||||
GetRoom(nodeID string) (string, bool)
|
||||
}
|
||||
|
||||
type RestGateway struct {
|
||||
influxGateway *influx.InfluxGateway
|
||||
mapping RoomMapper
|
||||
engine *gin.Engine
|
||||
measurementName string
|
||||
username string
|
||||
password string
|
||||
co2ThresholdMax int
|
||||
co2ThresholdMin int
|
||||
roomStatus map[string]*RoomCO2Status
|
||||
statusMu sync.RWMutex
|
||||
}
|
||||
|
||||
type RoomCO2Status struct {
|
||||
RoomID string `json:"room"`
|
||||
IsHigh bool `json:"is_high"`
|
||||
CO2 int `json:"co2"`
|
||||
}
|
||||
|
||||
func NewRestGateway(influxGateway *influx.InfluxGateway, mapping RoomMapper, measurementName string, username, password string) *RestGateway {
|
||||
maxThreshold, _ := strconv.Atoi(os.Getenv("CO2_THREASHOLD_MAX"))
|
||||
if maxThreshold == 0 {
|
||||
maxThreshold = 1400
|
||||
}
|
||||
minThreshold, _ := strconv.Atoi(os.Getenv("CO2_THREASHOLD_MIN"))
|
||||
if minThreshold == 0 {
|
||||
minThreshold = 1000
|
||||
}
|
||||
|
||||
g := &RestGateway{
|
||||
influxGateway: influxGateway,
|
||||
mapping: mapping,
|
||||
engine: gin.Default(),
|
||||
measurementName: measurementName,
|
||||
username: username,
|
||||
password: password,
|
||||
co2ThresholdMax: maxThreshold,
|
||||
co2ThresholdMin: minThreshold,
|
||||
roomStatus: make(map[string]*RoomCO2Status),
|
||||
}
|
||||
|
||||
g.setupRoutes()
|
||||
go g.runWatchdog()
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *RestGateway) setupRoutes() {
|
||||
// Setup CORS middleware to allow all *.e.kb28.ch origins
|
||||
corsConfig := cors.Config{
|
||||
AllowOriginFunc: func(origin string) bool {
|
||||
// Match any origin like *.e.kb28.ch
|
||||
pattern := regexp.MustCompile(`^https://.*\.e\.kb28\.ch$`)
|
||||
return pattern.MatchString(origin)
|
||||
},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Authorization", "Content-Type"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
}
|
||||
g.engine.Use(cors.New(corsConfig))
|
||||
|
||||
v1 := g.engine.Group("/api/v1")
|
||||
|
||||
// Public endpoints (no auth required)
|
||||
v1.GET("/battery", g.getBattery)
|
||||
|
||||
if g.username != "" && g.password != "" {
|
||||
v1.Use(gin.BasicAuth(gin.Accounts{
|
||||
g.username: g.password,
|
||||
}))
|
||||
}
|
||||
|
||||
{
|
||||
v1.GET("/rooms", g.getRooms)
|
||||
v1.GET("/rooms/:room-id/current", g.getRoomCurrent)
|
||||
v1.GET("/rooms/:room-id/history", g.getRoomHistory)
|
||||
v1.GET("/rooms/high-co2", g.getHighCO2Rooms)
|
||||
v1.GET("/export/csv", g.getExportCSV)
|
||||
}
|
||||
|
||||
g.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
|
||||
// buildNodeFilter builds a SQL WHERE clause fragment matching any of the given node IDs.
|
||||
func buildNodeFilter(nodes []string) string {
|
||||
quotedNodes := make([]string, len(nodes))
|
||||
for i, n := range nodes {
|
||||
quotedNodes[i] = fmt.Sprintf("'%s'", n)
|
||||
}
|
||||
return fmt.Sprintf(`"node" IN (%s)`, strings.Join(quotedNodes, ", "))
|
||||
}
|
||||
|
||||
// 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 '30 minutes'
|
||||
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 (30 min)"
|
||||
}
|
||||
result[room][nodeID] = map[string]any{"battery": battery}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// This function is 100% AI generated with Junie (Claude Sonnet 4.6)
|
||||
// GET /api/v1/export/csv
|
||||
// getExportCSV godoc
|
||||
// @Summary Export sensor data as CSV
|
||||
// @Description Export CO2, temperature, humidity, window status and battery data for a node over a time range
|
||||
// @Tags export
|
||||
// @Produce text/csv
|
||||
// @Param node query string true "Node MAC address (e.g. E8:F3:0A:F7:3B:F3)"
|
||||
// @Param from query string true "Start time in RFC3339 format (e.g. 2026-05-27T13:00:00Z)"
|
||||
// @Param to query string true "End time in RFC3339 format (e.g. 2026-05-27T15:00:00Z)"
|
||||
// @Success 200 {string} string "CSV file"
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Security BasicAuth
|
||||
// @Router /export/csv [get]
|
||||
func (g *RestGateway) getExportCSV(c *gin.Context) {
|
||||
node := c.Query("node")
|
||||
from := c.Query("from")
|
||||
to := c.Query("to")
|
||||
|
||||
if node == "" || from == "" || to == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "node, from and to query parameters are required"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := time.Parse(time.RFC3339, from); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "from must be a valid RFC3339 timestamp"})
|
||||
return
|
||||
}
|
||||
if _, err := time.Parse(time.RFC3339, to); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "to must be a valid RFC3339 timestamp"})
|
||||
return
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
time,
|
||||
co2_ppm,
|
||||
temp,
|
||||
battery,
|
||||
humidity,
|
||||
window_open
|
||||
FROM "%s"
|
||||
WHERE time >= '%s'
|
||||
AND time <= '%s'
|
||||
AND "node" = '%s'
|
||||
ORDER BY time ASC
|
||||
`, g.measurementName, from, to, node,
|
||||
)
|
||||
|
||||
it, err := g.influxGateway.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="export-%s.csv"`, strings.ReplaceAll(node, ":", "_")))
|
||||
c.Header("Content-Type", "text/csv")
|
||||
|
||||
w := csv.NewWriter(c.Writer)
|
||||
_ = w.Write([]string{"time", "co2", "temperature", "humidity", "windows", "battery"})
|
||||
|
||||
for it.Next() {
|
||||
row := it.Value()
|
||||
|
||||
var tsStr string
|
||||
if t, ok := row["time"]; ok {
|
||||
var tt time.Time
|
||||
switch v := t.(type) {
|
||||
case time.Time:
|
||||
tt = v
|
||||
case int64:
|
||||
tt = time.Unix(0, v)
|
||||
}
|
||||
loc := time.FixedZone("UTC+2", 2*60*60)
|
||||
tsStr = tt.In(loc).Format("2006-01-02T15:04:05")
|
||||
}
|
||||
|
||||
co2 := formatField(row["co2_ppm"])
|
||||
temp := formatField(row["temp"])
|
||||
humidity := formatField(row["humidity"])
|
||||
windowOpen := formatField(row["window_open"])
|
||||
battery := formatField(row["battery"])
|
||||
|
||||
_ = w.Write([]string{
|
||||
tsStr,
|
||||
co2,
|
||||
temp,
|
||||
humidity,
|
||||
windowOpen,
|
||||
battery,
|
||||
})
|
||||
}
|
||||
|
||||
if err := it.Err(); err != nil {
|
||||
// Headers already sent; best effort
|
||||
_ = w.Write([]string{"error", err.Error()})
|
||||
}
|
||||
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
// formatField converts an any value to its string representation for CSV output.
|
||||
func formatField(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return strconv.FormatFloat(val, 'f', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(val), 'f', -1, 32)
|
||||
case int64:
|
||||
return strconv.FormatInt(val, 10)
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(val), 10)
|
||||
case bool:
|
||||
if val {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// formatTime normalizes a time value (possibly a nanosecond int64) into an RFC3339 UTC string.
|
||||
func formatTime(v any) any {
|
||||
switch t := v.(type) {
|
||||
case time.Time:
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
case arrow.Timestamp:
|
||||
return time.Unix(0, int64(t)).UTC().Format(time.RFC3339)
|
||||
case int64:
|
||||
return time.Unix(0, t).UTC().Format(time.RFC3339)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RestGateway) Run(addr string) error {
|
||||
return g.engine.Run(addr)
|
||||
}
|
||||
|
||||
// GET /api/v1/rooms
|
||||
// getRooms godoc
|
||||
// @Summary Get all unique rooms
|
||||
// @Description Get a list of all unique rooms from the measurement
|
||||
// @Tags rooms
|
||||
// @Produce json
|
||||
// @Success 200 {array} string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Security BasicAuth
|
||||
// @Router /rooms [get]
|
||||
func (g *RestGateway) getRooms(c *gin.Context) {
|
||||
rooms := g.mapping.Rooms()
|
||||
slices.Sort(rooms)
|
||||
c.JSON(http.StatusOK, rooms)
|
||||
}
|
||||
|
||||
// GET /api/v1/rooms/{room-id}/current
|
||||
// getRoomCurrent godoc
|
||||
// @Summary Get current data for a room
|
||||
// @Description Get the latest record for a specific room
|
||||
// @Tags rooms
|
||||
// @Produce json
|
||||
// @Param room-id path string true "Room ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Security BasicAuth
|
||||
// @Router /rooms/{room-id}/current [get]
|
||||
func (g *RestGateway) getRoomCurrent(c *gin.Context) {
|
||||
roomID := c.Param("room-id")
|
||||
|
||||
nodes := g.mapping.NodesForRoom(roomID)
|
||||
if len(nodes) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Room not found in mapping"})
|
||||
return
|
||||
}
|
||||
nodeFilter := buildNodeFilter(nodes)
|
||||
|
||||
// Get the last record for the specific room by matching node IDs, aggregated by 5m intervals
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date_bin(INTERVAL '5 minutes', time)::TIMESTAMP AS time,
|
||||
ROUND(AVG(co2_ppm)) AS co2_ppm,
|
||||
ROUND(AVG(temp), 2) AS temp,
|
||||
ROUND(AVG(humidity), 2) AS humidity,
|
||||
MAX(window_open) AS window_open
|
||||
FROM "%s"
|
||||
WHERE time > now() - INTERVAL '1 day'
|
||||
AND %s
|
||||
GROUP BY date_bin(INTERVAL '5 minutes', time)
|
||||
ORDER BY time DESC
|
||||
LIMIT 1
|
||||
`, g.measurementName, nodeFilter,
|
||||
)
|
||||
|
||||
// Using context.Background() as seen in working snippet
|
||||
it, err := g.influxGateway.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if it.Next() {
|
||||
val := it.Value()
|
||||
val["room"] = roomID
|
||||
val["window"] = val["window_open"]
|
||||
delete(val, "window_open")
|
||||
if t, ok := val["time"]; ok {
|
||||
val["time"] = formatTime(t)
|
||||
}
|
||||
c.JSON(http.StatusOK, val)
|
||||
return
|
||||
}
|
||||
|
||||
if err := it.Err(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Room not found or no data available"})
|
||||
}
|
||||
|
||||
// GET /api/v1/rooms/{room-id}/history
|
||||
// getRoomHistory godoc
|
||||
// @Summary Get history for a room
|
||||
// @Description Get history for a specific room
|
||||
// @Tags rooms
|
||||
// @Produce json
|
||||
// @Param room-id path string true "Room ID"
|
||||
// @Param window query string false "Time window (e.g., 1 day, 1 hour, 30 min)" default(1 day)
|
||||
// @Success 200 {array} map[string]any
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Security BasicAuth
|
||||
// @Router /rooms/{room-id}/history [get]
|
||||
func (g *RestGateway) getRoomHistory(c *gin.Context) {
|
||||
roomID := c.Param("room-id")
|
||||
window := c.DefaultQuery("window", "1 day")
|
||||
|
||||
nodes := g.mapping.NodesForRoom(roomID)
|
||||
if len(nodes) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Room not found in mapping"})
|
||||
return
|
||||
}
|
||||
nodeFilter := buildNodeFilter(nodes)
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
WITH binned AS (
|
||||
SELECT
|
||||
date_bin(INTERVAL '1 minute', time)::TIMESTAMP AS time,
|
||||
AVG(co2_ppm) AS co2_ppm,
|
||||
AVG(temp) AS temp,
|
||||
AVG(humidity) AS humidity,
|
||||
MAX(window_open) AS window_open
|
||||
FROM "%s"
|
||||
WHERE time > now() - INTERVAL '%s' - INTERVAL '5 minutes'
|
||||
AND %s
|
||||
GROUP BY date_bin(INTERVAL '1 minute', time)
|
||||
)
|
||||
SELECT
|
||||
time,
|
||||
ROUND(AVG(co2_ppm) OVER (ORDER BY time RANGE BETWEEN INTERVAL '4 minutes' PRECEDING AND CURRENT ROW)) AS co2_ppm,
|
||||
ROUND(AVG(temp) OVER (ORDER BY time RANGE BETWEEN INTERVAL '4 minutes' PRECEDING AND CURRENT ROW), 2) AS temp,
|
||||
ROUND(AVG(humidity) OVER (ORDER BY time RANGE BETWEEN INTERVAL '4 minutes' PRECEDING AND CURRENT ROW), 2) AS humidity,
|
||||
window_open
|
||||
FROM binned
|
||||
WHERE time > now() - INTERVAL '%s'
|
||||
ORDER BY time DESC
|
||||
`, g.measurementName, window, nodeFilter, window,
|
||||
)
|
||||
|
||||
// Using context.Background() as seen in working snippet
|
||||
it, err := g.influxGateway.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var history []map[string]any
|
||||
for it.Next() {
|
||||
val := it.Value()
|
||||
val["room"] = roomID
|
||||
val["window"] = val["window_open"]
|
||||
delete(val, "window_open")
|
||||
if t, ok := val["time"]; ok {
|
||||
val["time"] = formatTime(t)
|
||||
}
|
||||
history = append(history, val)
|
||||
}
|
||||
|
||||
if err := it.Err(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, history)
|
||||
}
|
||||
|
||||
func (g *RestGateway) runWatchdog() {
|
||||
// Initial check
|
||||
g.checkCO2()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
g.checkCO2()
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RestGateway) checkCO2() {
|
||||
rooms := g.mapping.Rooms()
|
||||
for _, roomID := range rooms {
|
||||
nodes := g.mapping.NodesForRoom(roomID)
|
||||
if len(nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
nodeFilter := buildNodeFilter(nodes)
|
||||
|
||||
// Get the last record for the specific room by matching node IDs, aggregated by 5m intervals
|
||||
// Same logic as getRoomCurrent
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
date_bin(INTERVAL '5 minutes', time)::TIMESTAMP AS time,
|
||||
ROUND(AVG(co2_ppm)) AS co2_ppm
|
||||
FROM "%s"
|
||||
WHERE time > now() - INTERVAL '1 day'
|
||||
AND %s
|
||||
GROUP BY date_bin(INTERVAL '5 minutes', time)
|
||||
ORDER BY time ASC
|
||||
LIMIT 1
|
||||
`, g.measurementName, nodeFilter,
|
||||
)
|
||||
|
||||
it, err := g.influxGateway.Query(context.Background(), query)
|
||||
if err != nil {
|
||||
log.Printf("[Watchdog] Error querying CO2 for room %s: %v\n", roomID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if it.Next() {
|
||||
val := it.Value()
|
||||
if co2Val, ok := val["co2_ppm"]; ok {
|
||||
co2 := int(co2Val.(float64))
|
||||
|
||||
g.statusMu.Lock()
|
||||
status, ok := g.roomStatus[roomID]
|
||||
if !ok {
|
||||
status = &RoomCO2Status{RoomID: roomID}
|
||||
g.roomStatus[roomID] = status
|
||||
}
|
||||
status.CO2 = co2
|
||||
|
||||
if co2 > g.co2ThresholdMax {
|
||||
status.IsHigh = true
|
||||
} else if co2 < g.co2ThresholdMin {
|
||||
status.IsHigh = false
|
||||
}
|
||||
g.statusMu.Unlock()
|
||||
}
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
log.Printf("[Watchdog] Error in iterator for room %s: %v\n", roomID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/v1/rooms/high-co2
|
||||
// getHighCO2Rooms godoc
|
||||
// @Summary Get rooms with high CO2 levels
|
||||
// @Description Get a list of rooms where CO2 levels are above the threshold
|
||||
// @Tags rooms
|
||||
// @Produce json
|
||||
// @Success 200 {array} RoomCO2Status
|
||||
// @Security BasicAuth
|
||||
// @Router /rooms/high-co2 [get]
|
||||
func (g *RestGateway) getHighCO2Rooms(c *gin.Context) {
|
||||
g.statusMu.RLock()
|
||||
defer g.statusMu.RUnlock()
|
||||
|
||||
var result = []RoomCO2Status{}
|
||||
for _, status := range g.roomStatus {
|
||||
if status.IsHigh {
|
||||
result = append(result, *status)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by room ID for stability
|
||||
slices.SortFunc(result, func(a, b RoomCO2Status) int {
|
||||
return strings.Compare(a.RoomID, b.RoomID)
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
http:
|
||||
middlewares:
|
||||
pi-db-doc-redirect:
|
||||
redirectRegex:
|
||||
regex: "^https://doc.db.e.kb28.ch/$"
|
||||
replacement: "https://doc.db.e.kb28.ch/swagger/index.html"
|
||||
# middlewares:
|
||||
# oidc-auth-pi-db:
|
||||
# plugin:
|
||||
@@ -10,15 +15,22 @@ http:
|
||||
# ClientId: ""
|
||||
# ClientSecret: ""
|
||||
routers:
|
||||
pi-db-ui:
|
||||
rule: "Host(`ui.db.e.kb28.ch`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: pi-db-ui
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
# middlewares:
|
||||
# - oidc-auth-pi-db@file
|
||||
pi-db:
|
||||
rule: "Host(`ui.e.kb28.ch`)"
|
||||
rule: "Host(`db.e.kb28.ch`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: pi-db
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
# middlewares:
|
||||
# - oidc-auth-pi-db@file
|
||||
pi-mqtt-management:
|
||||
rule: "Host(`mqtt.e.kb28.ch`)"
|
||||
entryPoints:
|
||||
@@ -26,18 +38,55 @@ http:
|
||||
service: pi-mqtt-management
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
pi-db-api:
|
||||
rule: "Host(`api.db.e.kb28.ch`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: pi-db-api
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
pi-db-doc:
|
||||
rule: "Host(`doc.db.e.kb28.ch`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: pi-db-api
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
middlewares:
|
||||
- pi-db-doc-redirect
|
||||
pi-ui:
|
||||
rule: "Host(`ui.e.kb28.ch`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: pi-ui
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
pi-db:
|
||||
pi-db-ui:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.42.211:8093"
|
||||
passHostHeader: true
|
||||
pi-db:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "h2c://192.168.42.211:8181"
|
||||
passHostHeader: true
|
||||
pi-mqtt-management:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.42.211:15672"
|
||||
passHostHeader: true
|
||||
pi-db-api:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.42.211:8080"
|
||||
passHostHeader: true
|
||||
pi-ui:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.42.211:80"
|
||||
|
||||
tcp:
|
||||
routers:
|
||||
|
||||
@@ -15,6 +15,7 @@ class "Window_status" as win{}
|
||||
class "Hygrometer" as hygro{}
|
||||
class "Thermometer" as thermo{}
|
||||
class "CO2_level" as co2{}
|
||||
class "Battery_level" as batt{}
|
||||
|
||||
sup o-d- ble
|
||||
sup o-u- sens
|
||||
@@ -22,5 +23,6 @@ sens <|-l- win
|
||||
sens <|-u- hygro
|
||||
sens <|-u- thermo
|
||||
sens <|-r- co2
|
||||
sens <|-- batt
|
||||
|
||||
@enduml
|
||||
|
||||
4
model/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
pandas
|
||||
matplotlib
|
||||
numpy
|
||||
openpyxl
|
||||
8
nodes/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(plein_de_eeeeeeeeeeee)
|
||||
|
||||
target_sources(app PRIVATE src/main.c src/window_status.c src/window_status.h src/thermometer.c src/thermometer.h src/hygrometer.c src/hygrometer.h src/co2_level.c src/co2_level.h src/supervisor.c src/supervisor.h src/ble_advertiser.c src/ble_advertiser.h src/battery_percent.h src/battery_percent.c)
|
||||
target_sources(app PRIVATE src/modules/battery.h src/modules/battery.c)
|
||||
11
nodes/app.overlay
Normal file
@@ -0,0 +1,11 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
/ {
|
||||
buttons{
|
||||
compatible = "gpio-keys";
|
||||
window_switch: window_switch {
|
||||
gpios = <&sx1509b 0 GPIO_ACTIVE_LOW>;
|
||||
label = "Window Switch";
|
||||
};
|
||||
};
|
||||
};
|
||||
16
nodes/prj.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
CONFIG_ADC=y
|
||||
CONFIG_I2C=y
|
||||
CONFIG_SENSOR=y
|
||||
CONFIG_BT=y
|
||||
CONFIG_GPIO=y
|
||||
|
||||
CONFIG_LOG=n
|
||||
CONFIG_CONSOLE=n
|
||||
CONFIG_SERIAL=n
|
||||
CONFIG_PRINTK=n
|
||||
CONFIG_UART_CONSOLE=n
|
||||
|
||||
CONFIG_PM=y
|
||||
CONFIG_PM_DEVICE=y
|
||||
CONFIG_PM_DEVICE_RUNTIME=y
|
||||
CONFIG_CCS811_DRIVE_MODE_3=y
|
||||
42
nodes/src/battery_percent.c
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "battery_percent.h"
|
||||
|
||||
/**
|
||||
* Taken from sample/board/nordic/battery/src/main.c
|
||||
* A discharge curve specific to the power source.
|
||||
*/
|
||||
static const struct battery_level_point levels[] = {
|
||||
/* "Curve" here eyeballed from captured data for the [Adafruit
|
||||
* 3.7v 2000 mAh](https://www.adafruit.com/product/2011) LIPO
|
||||
* under full load that started with a charge of 3.96 V and
|
||||
* dropped about linearly to 3.58 V over 15 hours. It then
|
||||
* dropped rapidly to 3.10 V over one hour, at which point it
|
||||
* stopped transmitting.
|
||||
*
|
||||
* Based on eyeball comparisons we'll say that 15/16 of life
|
||||
* goes between 3.95 and 3.55 V, and 1/16 goes between 3.55 V
|
||||
* and 3.1 V.
|
||||
*/
|
||||
|
||||
{ 10000, 3950 },
|
||||
{ 625, 3550 },
|
||||
{ 0, 3100 },
|
||||
};
|
||||
|
||||
enum error_code battery_init(){
|
||||
enum error_code ret = init_failed;
|
||||
if(0 == battery_measure_enable(true)){
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum error_code battery_get_value(int* holder){
|
||||
enum error_code ret = read_failed;
|
||||
// battery_voltage in [mV]
|
||||
int battery_voltage = battery_sample();
|
||||
if(battery_voltage >= 0){
|
||||
*holder = (battery_level_pptt(battery_voltage, levels)/100);
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
27
nodes/src/battery_percent.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef BATTERY_PERCENT_H
|
||||
#define BATTERY_PERCENT_H
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
#include "modules/battery.h"
|
||||
|
||||
#include "error_code.h"
|
||||
|
||||
/**
|
||||
* @brief init the battery module
|
||||
* @return init_failed upon any error during init
|
||||
* @return success otherwise
|
||||
*/
|
||||
enum error_code battery_init();
|
||||
|
||||
/**
|
||||
* @brief Retrieve the battery charge percentage and stores it in the given parameter
|
||||
* @param[out] holder : pointer where the measurement will be stored
|
||||
* @return read_failed upon any error occuring during measurement
|
||||
* @return success otherwise
|
||||
* @note the content of holder should not be trusted upon returning something else than success
|
||||
*/
|
||||
enum error_code battery_get_value(int* holder);
|
||||
|
||||
#endif //BATTERY_PERCENT_H
|
||||
|
||||
79
nodes/src/ble_advertiser.c
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "ble_advertiser.h"
|
||||
|
||||
static const int BT_MS_ADV_UP = 500; // [ms] time during which the advertising is active
|
||||
|
||||
// keys as defined in the specs
|
||||
static const char BT_KEY_SIZE = 1; // [B]
|
||||
static const char BT_KEY_WINDOW = 0x01;
|
||||
static const char BT_KEY_HUMIDITY = 0x02;
|
||||
static const char BT_KEY_TEMP = 0x03;
|
||||
static const char BT_KEY_CO2_LVL = 0x04;
|
||||
static const char BT_KEY_BATT = 0x05;
|
||||
|
||||
// value size [B]
|
||||
static const char BT_PREAMBLE_SIZE = 2;
|
||||
static const char BT_VALUE_SIZE_WINDOW = 1;
|
||||
static const char BT_VALUE_SIZE_HUMIDITY = 1;
|
||||
static const char BT_VALUE_SIZE_TEMP = 2;
|
||||
static const char BT_VALUE_SIZE_CO2_LVL = 4;
|
||||
static const char BT_VALUE_SIZE_BATT = 1;
|
||||
|
||||
// value start index in the frame - equal to {prior field index + prior value size + key size}
|
||||
static const int BT_AD_DATA_INDEX_WINDOW = BT_PREAMBLE_SIZE + BT_KEY_SIZE;
|
||||
static const int BT_AD_DATA_INDEX_HUMIDITY = BT_AD_DATA_INDEX_WINDOW + BT_VALUE_SIZE_WINDOW + BT_KEY_SIZE;
|
||||
static const int BT_AD_DATA_INDEX_TEMP = BT_AD_DATA_INDEX_HUMIDITY + BT_VALUE_SIZE_HUMIDITY + BT_KEY_SIZE;
|
||||
static const int BT_AD_DATA_INDEX_CO2_LVL = BT_AD_DATA_INDEX_TEMP + BT_VALUE_SIZE_TEMP + BT_KEY_SIZE;
|
||||
static const int BT_AD_DATA_INDEX_BATT = BT_AD_DATA_INDEX_CO2_LVL + BT_VALUE_SIZE_CO2_LVL + BT_KEY_SIZE;
|
||||
|
||||
// sum of all value size + size for all keys + size preamble
|
||||
static const char BT_AD_TOTAL_SIZE =
|
||||
BT_PREAMBLE_SIZE + (5 * BT_KEY_SIZE) +
|
||||
BT_VALUE_SIZE_WINDOW + BT_VALUE_SIZE_HUMIDITY + BT_VALUE_SIZE_TEMP + BT_VALUE_SIZE_CO2_LVL + BT_VALUE_SIZE_BATT;
|
||||
|
||||
// data field in the broadcasted frame
|
||||
// all values are set to 0 and the "company id" field is set to 0xffff
|
||||
static uint8_t ad_data[] = {
|
||||
0xff, 0xff,
|
||||
BT_KEY_WINDOW, 0x00,
|
||||
BT_KEY_HUMIDITY, 0x00,
|
||||
BT_KEY_TEMP, 0x00, 0x00,
|
||||
BT_KEY_CO2_LVL, 0x00, 0x00, 0x00, 0x00,
|
||||
BT_KEY_BATT, 0x00
|
||||
};
|
||||
// frame that will be broadcasted
|
||||
static const struct bt_data ad[] = {
|
||||
BT_DATA(BT_DATA_MANUFACTURER_DATA, ad_data, BT_AD_TOTAL_SIZE),
|
||||
};
|
||||
|
||||
enum error_code ble_init(){
|
||||
return ((0 == bt_enable(NULL)) ? success : init_failed);
|
||||
}
|
||||
|
||||
enum error_code ble_advertise(
|
||||
enum window_status window_value,
|
||||
int hygro_value,
|
||||
int thermo_value,
|
||||
int co2_lvl_value,
|
||||
int batt_value
|
||||
){
|
||||
enum error_code ret = write_failed;
|
||||
int shift = 0;
|
||||
ad_data[BT_AD_DATA_INDEX_WINDOW] = (uint8_t)(window_value == open ? 0x1 : 0x0);
|
||||
ad_data[BT_AD_DATA_INDEX_HUMIDITY] = (uint8_t)(hygro_value & 0xff);
|
||||
ad_data[BT_AD_DATA_INDEX_BATT] = (uint8_t)(batt_value & 0xff);
|
||||
for(int i=0;i<BT_VALUE_SIZE_TEMP;i++){
|
||||
shift = 8 * (BT_VALUE_SIZE_TEMP - i - 1);
|
||||
ad_data[BT_AD_DATA_INDEX_TEMP + i] = (uint8_t)((thermo_value>>(shift)) & 0xff);
|
||||
}
|
||||
for(int i=0;i<BT_VALUE_SIZE_CO2_LVL;i++){
|
||||
shift = 8 * (BT_VALUE_SIZE_CO2_LVL - i - 1);
|
||||
ad_data[BT_AD_DATA_INDEX_CO2_LVL + i] = (uint8_t)((co2_lvl_value>>(shift)) & 0xff);
|
||||
}
|
||||
if(0 == bt_le_adv_start(BT_LE_ADV_NCONN_IDENTITY, ad, ARRAY_SIZE(ad), NULL, 0)){
|
||||
k_msleep(BT_MS_ADV_UP);
|
||||
if(0 == bt_le_adv_stop()){
|
||||
ret = success;
|
||||
}else{}
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
37
nodes/src/ble_advertiser.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef BLE_ADVERTISER_H
|
||||
#define BLE_ADVERTISER_H
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/bluetooth/assigned_numbers.h>
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
|
||||
#include "error_code.h"
|
||||
#include "window_status.h"
|
||||
|
||||
/**
|
||||
* @brief Initialise the ble module
|
||||
* @return init_failed upon any error occuring during init
|
||||
* @return success otherwise
|
||||
*/
|
||||
enum error_code ble_init();
|
||||
|
||||
/**
|
||||
* @brief Sets the given values as data and broadcast the BLE frame
|
||||
* @param window_value : window opening status in {open/closed}
|
||||
* @param hygro_value : humidity percentage [%]
|
||||
* @param thermo_value : temperature in [d°C]
|
||||
* @param co2_lvl_value : co2 level in [ppm]
|
||||
* @param battery_percent_value : battery current level of charge [%]
|
||||
* @return write_failed if any problem occur during the BLE broadcast
|
||||
* @return success otherwise
|
||||
* @note The broadcast is stopped at the return time
|
||||
*/
|
||||
enum error_code ble_advertise(
|
||||
enum window_status window_value,
|
||||
int hygro_value,
|
||||
int thermo_value,
|
||||
int co2_lvl_value,
|
||||
int batt_value
|
||||
);
|
||||
|
||||
#endif //BLE_ADVERTISER_H
|
||||
37
nodes/src/co2_level.c
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "co2_level.h"
|
||||
|
||||
static const struct device* dev = DEVICE_DT_GET_ONE(ams_ccs811);
|
||||
|
||||
const int CO2_LEVEL_EMPTY_ROOM = 400; // [ppm]
|
||||
|
||||
enum error_code co2_lvl_init(){
|
||||
enum error_code ret = init_failed;
|
||||
if(device_is_ready(dev)){
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum error_code co2_lvl_get_value(int* holder){
|
||||
struct sensor_value temp, humidity, co2;
|
||||
enum error_code ret = read_failed;
|
||||
int temp_value, humidity_value;
|
||||
if( (success == thermo_get_value(&temp_value)) && (success == hygro_get_value(&humidity_value)) ){
|
||||
// temperature conversion from deci, no function in the API
|
||||
temp.val1 = temp_value/10;
|
||||
temp.val2 = temp_value%10;
|
||||
// humidity conversion is straight away
|
||||
humidity.val1 = humidity_value;
|
||||
if(
|
||||
// co2 measurement requires temperature and humidity
|
||||
(0 == ccs811_envdata_update(dev, &temp, &humidity)) &&
|
||||
// fetch is required to update sensor read data
|
||||
(0 == sensor_sample_fetch(dev)) &&
|
||||
(0 == sensor_channel_get(dev, SENSOR_CHAN_CO2, &co2))
|
||||
){
|
||||
*holder = co2.val1; // taking only the integer part
|
||||
ret = success;
|
||||
}else{}
|
||||
}else{}
|
||||
return success;
|
||||
}
|
||||
30
nodes/src/co2_level.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef CO2_LEVEL_H
|
||||
#define CO2_LEVEL_H
|
||||
|
||||
#include <zephyr/drivers/sensor.h>
|
||||
#include <zephyr/drivers/sensor/ccs811.h>
|
||||
|
||||
#include "error_code.h"
|
||||
#include "hygrometer.h"
|
||||
#include "thermometer.h"
|
||||
|
||||
extern const int CO2_LEVEL_EMPTY_ROOM; // [ppm]
|
||||
|
||||
/**
|
||||
* @brief init the co2 level measurement module
|
||||
* @return init_failed upon any error during init
|
||||
* @return success otherwise
|
||||
*/
|
||||
enum error_code co2_lvl_init();
|
||||
|
||||
/**
|
||||
* @brief Retrieve the CO2 level measurement and stores it in the given parameter
|
||||
* @param[out] holder : pointer where the measurement will be stored
|
||||
* @return read_failed upon any error occuring during measurement
|
||||
* @return success otherwise
|
||||
* @note the content of holder should not be trusted upon returning something else than success
|
||||
* @note the value is not read for the first few measurements after a reboot
|
||||
*/
|
||||
enum error_code co2_lvl_get_value(int* holder);
|
||||
|
||||
#endif //CO2_LEVEL_H
|
||||
16
nodes/src/error_code.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef ERROR_CODE_H
|
||||
#define ERROR_CODE_H
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
// enum of all error code that may occur during the plein_de_eeeeeeeeeeeeeee application runtime
|
||||
enum error_code{
|
||||
success = 0, // any kind of success
|
||||
init_failed, // error related to initialization
|
||||
read_failed, // error related to reading action
|
||||
write_failed, // error related to writing action
|
||||
error_unknown, // any error not linked to other code
|
||||
error_code_last, // iteration purpose
|
||||
};
|
||||
|
||||
#endif //ERROR_CODE_H
|
||||
25
nodes/src/hygrometer.c
Normal file
@@ -0,0 +1,25 @@
|
||||
#include "hygrometer.h"
|
||||
|
||||
static const struct device* dev = DEVICE_DT_GET_ONE(st_hts221);
|
||||
|
||||
enum error_code hygro_init(){
|
||||
enum error_code ret = init_failed;
|
||||
if(device_is_ready(dev)){
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum error_code hygro_get_value(int* holder){
|
||||
enum error_code ret = read_failed;
|
||||
struct sensor_value humidity;
|
||||
if(
|
||||
// fetch is required to update sensor read data
|
||||
(sensor_sample_fetch(dev) >= 0) &&
|
||||
(sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY, &humidity) >= 0)
|
||||
){
|
||||
*holder = humidity.val1; //taking only the integer part
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
26
nodes/src/hygrometer.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef HYGROMETER_H
|
||||
#define HYGROMETER_H
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/sensor.h>
|
||||
|
||||
#include "error_code.h"
|
||||
|
||||
/**
|
||||
* @brief init the hygrometer module
|
||||
* @return init_failed upon any error during init
|
||||
* @return success otherwise
|
||||
*/
|
||||
enum error_code hygro_init();
|
||||
|
||||
/**
|
||||
* @brief Retrieve the humidity level and stores it in the given parameter
|
||||
* @param[out] holder : pointer where the measurement will be stored
|
||||
* @return read_failed upon any error occuring during measurement
|
||||
* @return success otherwise
|
||||
* @note the content of holder should not be trusted upon returning something else than success
|
||||
*/
|
||||
enum error_code hygro_get_value(int* holder);
|
||||
|
||||
#endif //HYGROMETER_H
|
||||
18
nodes/src/main.c
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2016 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
|
||||
#include "supervisor.h"
|
||||
|
||||
// size of a stack used by the supervisor thread
|
||||
#define STACKSIZE 1024
|
||||
// scheduling priority used by the supervisor thread
|
||||
#define PRIORITY 7
|
||||
|
||||
K_THREAD_DEFINE(supervisor_id, STACKSIZE, thread_supervisor, NULL, NULL, NULL, PRIORITY, 0, 0);
|
||||
231
nodes/src/modules/battery.c
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Peter Bigot Consulting, LLC
|
||||
* Copyright (c) 2019-2020 Nordic Semiconductor ASA
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/init.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/adc.h>
|
||||
#include <zephyr/drivers/sensor.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "battery.h"
|
||||
|
||||
LOG_MODULE_REGISTER(BATTERY, CONFIG_ADC_LOG_LEVEL);
|
||||
|
||||
#define VBATT DT_PATH(vbatt)
|
||||
#define ZEPHYR_USER DT_PATH(zephyr_user)
|
||||
|
||||
#ifdef CONFIG_BOARD_THINGY52_NRF52832
|
||||
/* This board uses a divider that reduces max voltage to
|
||||
* reference voltage (600 mV).
|
||||
*/
|
||||
#define BATTERY_ADC_GAIN ADC_GAIN_1
|
||||
#else
|
||||
/* Other boards may use dividers that only reduce battery voltage to
|
||||
* the maximum supported by the hardware (3.6 V)
|
||||
*/
|
||||
#define BATTERY_ADC_GAIN ADC_GAIN_1_6
|
||||
#endif
|
||||
|
||||
struct io_channel_config {
|
||||
uint8_t channel;
|
||||
};
|
||||
|
||||
struct divider_config {
|
||||
struct io_channel_config io_channel;
|
||||
struct gpio_dt_spec power_gpios;
|
||||
/* output_ohm is used as a flag value: if it is nonzero then
|
||||
* the battery is measured through a voltage divider;
|
||||
* otherwise it is assumed to be directly connected to Vdd.
|
||||
*/
|
||||
uint32_t output_ohm;
|
||||
uint32_t full_ohm;
|
||||
};
|
||||
|
||||
static const struct divider_config divider_config = {
|
||||
#if DT_NODE_HAS_STATUS_OKAY(VBATT)
|
||||
.io_channel = {
|
||||
DT_IO_CHANNELS_INPUT(VBATT),
|
||||
},
|
||||
.power_gpios = GPIO_DT_SPEC_GET_OR(VBATT, power_gpios, {}),
|
||||
.output_ohm = DT_PROP(VBATT, output_ohms),
|
||||
.full_ohm = DT_PROP(VBATT, full_ohms),
|
||||
#else /* /vbatt exists */
|
||||
.io_channel = {
|
||||
DT_IO_CHANNELS_INPUT(ZEPHYR_USER),
|
||||
},
|
||||
#endif /* /vbatt exists */
|
||||
};
|
||||
|
||||
struct divider_data {
|
||||
const struct device *adc;
|
||||
struct adc_channel_cfg adc_cfg;
|
||||
struct adc_sequence adc_seq;
|
||||
int16_t raw;
|
||||
};
|
||||
static struct divider_data divider_data = {
|
||||
#if DT_NODE_HAS_STATUS_OKAY(VBATT)
|
||||
.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(VBATT)),
|
||||
#else
|
||||
.adc = DEVICE_DT_GET(DT_IO_CHANNELS_CTLR(ZEPHYR_USER)),
|
||||
#endif
|
||||
};
|
||||
|
||||
static int divider_setup(void)
|
||||
{
|
||||
const struct divider_config *cfg = ÷r_config;
|
||||
const struct io_channel_config *iocp = &cfg->io_channel;
|
||||
const struct gpio_dt_spec *gcp = &cfg->power_gpios;
|
||||
struct divider_data *ddp = ÷r_data;
|
||||
struct adc_sequence *asp = &ddp->adc_seq;
|
||||
struct adc_channel_cfg *accp = &ddp->adc_cfg;
|
||||
int rc;
|
||||
|
||||
if (!device_is_ready(ddp->adc)) {
|
||||
LOG_ERR("ADC device is not ready %s", ddp->adc->name);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
if (gcp->port) {
|
||||
if (!device_is_ready(gcp->port)) {
|
||||
LOG_ERR("%s: device not ready", gcp->port->name);
|
||||
return -ENOENT;
|
||||
}
|
||||
rc = gpio_pin_configure_dt(gcp, GPIO_OUTPUT_INACTIVE);
|
||||
if (rc != 0) {
|
||||
LOG_ERR("Failed to control feed %s.%u: %d",
|
||||
gcp->port->name, gcp->pin, rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
*asp = (struct adc_sequence){
|
||||
.channels = BIT(0),
|
||||
.buffer = &ddp->raw,
|
||||
.buffer_size = sizeof(ddp->raw),
|
||||
.oversampling = 4,
|
||||
.calibrate = true,
|
||||
};
|
||||
|
||||
#ifdef CONFIG_ADC_NRFX_SAADC
|
||||
*accp = (struct adc_channel_cfg){
|
||||
.gain = BATTERY_ADC_GAIN,
|
||||
.reference = ADC_REF_INTERNAL,
|
||||
.acquisition_time = ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40),
|
||||
};
|
||||
|
||||
if (cfg->output_ohm != 0) {
|
||||
accp->input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0
|
||||
+ iocp->channel;
|
||||
} else {
|
||||
accp->input_positive = SAADC_CH_PSELP_PSELP_VDD;
|
||||
}
|
||||
|
||||
asp->resolution = 14;
|
||||
#else /* CONFIG_ADC_var */
|
||||
#error Unsupported ADC
|
||||
#endif /* CONFIG_ADC_var */
|
||||
|
||||
rc = adc_channel_setup(ddp->adc, accp);
|
||||
LOG_INF("Setup AIN%u got %d", iocp->channel, rc);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static bool battery_ok;
|
||||
|
||||
static int battery_setup(void)
|
||||
{
|
||||
int rc = divider_setup();
|
||||
|
||||
battery_ok = (rc == 0);
|
||||
LOG_INF("Battery setup: %d %d", rc, battery_ok);
|
||||
return rc;
|
||||
}
|
||||
|
||||
SYS_INIT(battery_setup, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
|
||||
|
||||
int battery_measure_enable(bool enable)
|
||||
{
|
||||
int rc = -ENOENT;
|
||||
|
||||
if (battery_ok) {
|
||||
const struct gpio_dt_spec *gcp = ÷r_config.power_gpios;
|
||||
|
||||
rc = 0;
|
||||
if (gcp->port) {
|
||||
rc = gpio_pin_set_dt(gcp, enable);
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int battery_sample(void)
|
||||
{
|
||||
int rc = -ENOENT;
|
||||
|
||||
if (battery_ok) {
|
||||
struct divider_data *ddp = ÷r_data;
|
||||
const struct divider_config *dcp = ÷r_config;
|
||||
struct adc_sequence *sp = &ddp->adc_seq;
|
||||
|
||||
rc = adc_read(ddp->adc, sp);
|
||||
sp->calibrate = false;
|
||||
if (rc == 0) {
|
||||
int32_t val = ddp->raw;
|
||||
|
||||
adc_raw_to_millivolts(adc_ref_internal(ddp->adc),
|
||||
ddp->adc_cfg.gain,
|
||||
sp->resolution,
|
||||
&val);
|
||||
|
||||
if (dcp->output_ohm != 0) {
|
||||
rc = val * (uint64_t)dcp->full_ohm
|
||||
/ dcp->output_ohm;
|
||||
LOG_INF("raw %u ~ %u mV => %d mV\n",
|
||||
ddp->raw, val, rc);
|
||||
} else {
|
||||
rc = val;
|
||||
LOG_INF("raw %u ~ %u mV\n", ddp->raw, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
unsigned int battery_level_pptt(unsigned int batt_mV,
|
||||
const struct battery_level_point *curve)
|
||||
{
|
||||
const struct battery_level_point *pb = curve;
|
||||
|
||||
if (batt_mV >= pb->lvl_mV) {
|
||||
/* Measured voltage above highest point, cap at maximum. */
|
||||
return pb->lvl_pptt;
|
||||
}
|
||||
/* Go down to the last point at or below the measured voltage. */
|
||||
while ((pb->lvl_pptt > 0)
|
||||
&& (batt_mV < pb->lvl_mV)) {
|
||||
++pb;
|
||||
}
|
||||
if (batt_mV < pb->lvl_mV) {
|
||||
/* Below lowest point, cap at minimum */
|
||||
return pb->lvl_pptt;
|
||||
}
|
||||
|
||||
/* Linear interpolation between below and above points. */
|
||||
const struct battery_level_point *pa = pb - 1;
|
||||
|
||||
return pb->lvl_pptt
|
||||
+ ((pa->lvl_pptt - pb->lvl_pptt)
|
||||
* (batt_mV - pb->lvl_mV)
|
||||
/ (pa->lvl_mV - pb->lvl_mV));
|
||||
}
|
||||
53
nodes/src/modules/battery.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2019 Peter Bigot Consulting, LLC
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#ifndef APPLICATION_BATTERY_H_
|
||||
#define APPLICATION_BATTERY_H_
|
||||
|
||||
/** Enable or disable measurement of the battery voltage.
|
||||
*
|
||||
* @param enable true to enable, false to disable
|
||||
*
|
||||
* @return zero on success, or a negative error code.
|
||||
*/
|
||||
int battery_measure_enable(bool enable);
|
||||
|
||||
/** Measure the battery voltage.
|
||||
*
|
||||
* @return the battery voltage in millivolts, or a negative error
|
||||
* code.
|
||||
*/
|
||||
int battery_sample(void);
|
||||
|
||||
/** A point in a battery discharge curve sequence.
|
||||
*
|
||||
* A discharge curve is defined as a sequence of these points, where
|
||||
* the first point has #lvl_pptt set to 10000 and the last point has
|
||||
* #lvl_pptt set to zero. Both #lvl_pptt and #lvl_mV should be
|
||||
* monotonic decreasing within the sequence.
|
||||
*/
|
||||
struct battery_level_point {
|
||||
/** Remaining life at #lvl_mV. */
|
||||
uint16_t lvl_pptt;
|
||||
|
||||
/** Battery voltage at #lvl_pptt remaining life. */
|
||||
uint16_t lvl_mV;
|
||||
};
|
||||
|
||||
/** Calculate the estimated battery level based on a measured voltage.
|
||||
*
|
||||
* @param batt_mV a measured battery voltage level.
|
||||
*
|
||||
* @param curve the discharge curve for the type of battery installed
|
||||
* on the system.
|
||||
*
|
||||
* @return the estimated remaining capacity in parts per ten
|
||||
* thousand.
|
||||
*/
|
||||
unsigned int battery_level_pptt(unsigned int batt_mV,
|
||||
const struct battery_level_point *curve);
|
||||
|
||||
#endif /* APPLICATION_BATTERY_H_ */
|
||||
75
nodes/src/supervisor.c
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "supervisor.h"
|
||||
|
||||
const int SLEEP_GRANULARITY = 1; // [min]
|
||||
const int SLEEP_MIN_DURATION = SLEEP_GRANULARITY; // [min]
|
||||
const int SLEEP_MAX_DURATION = 30; // [min]
|
||||
|
||||
// Zephyr related stuff
|
||||
|
||||
void thread_supervisor(){
|
||||
supervisor_init();
|
||||
supervisor_run();
|
||||
//should never return
|
||||
}
|
||||
|
||||
// supervisor stuff
|
||||
|
||||
enum error_code supervisor_init(){
|
||||
enum error_code ret = init_failed;
|
||||
if(
|
||||
success == ble_init() &&
|
||||
success == co2_lvl_init() &&
|
||||
success == hygro_init() &&
|
||||
success == thermo_init() &&
|
||||
success == window_init() &&
|
||||
success == battery_init()
|
||||
){
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum error_code supervisor_run(){
|
||||
int co2_lvl_value = -1;
|
||||
int hygro_value = -1;
|
||||
int thermo_value = -1;
|
||||
int batt_value = -1;
|
||||
enum window_status window_value = unknown;
|
||||
enum error_code co2_lvl_status, hygro_status, thermo_status, window_status, batt_status;
|
||||
int current_sleep_time = SLEEP_MIN_DURATION;
|
||||
while(1){
|
||||
co2_lvl_status = co2_lvl_get_value(&co2_lvl_value);
|
||||
hygro_status = hygro_get_value(&hygro_value);
|
||||
thermo_status = thermo_get_value(&thermo_value);
|
||||
window_status = window_get_value(&window_value);
|
||||
batt_status = battery_get_value(&batt_value);
|
||||
if(success != window_status){
|
||||
window_value = unknown;
|
||||
}else{}
|
||||
if(success != hygro_status){
|
||||
hygro_value = -1;
|
||||
}else{}
|
||||
if(success != thermo_status){
|
||||
thermo_value = -1;
|
||||
}else{}
|
||||
if(success != co2_lvl_status){
|
||||
co2_lvl_value = -1;
|
||||
}else{}
|
||||
if(success != batt_status){
|
||||
batt_value = -1;
|
||||
}else{}
|
||||
ble_advertise(window_value, hygro_value, thermo_value, co2_lvl_value, batt_value);
|
||||
if((co2_lvl_value > CO2_LEVEL_EMPTY_ROOM) || (window_value == open)){
|
||||
// there are people in the room, or someone forgot to close the window
|
||||
current_sleep_time = SLEEP_MIN_DURATION;
|
||||
}else{
|
||||
// no one is in the room, we can wait a litle bit longer before getting the next data point
|
||||
current_sleep_time += SLEEP_GRANULARITY;
|
||||
if(current_sleep_time > SLEEP_MAX_DURATION){
|
||||
current_sleep_time = SLEEP_MAX_DURATION;
|
||||
}else{}
|
||||
}
|
||||
k_sleep(K_MINUTES(current_sleep_time));
|
||||
}
|
||||
return error_unknown; // should never return
|
||||
}
|
||||
38
nodes/src/supervisor.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef SUPERVISOR_H
|
||||
#define SUPERVISOR_H
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
#include "error_code.h"
|
||||
|
||||
#include "ble_advertiser.h"
|
||||
#include "window_status.h"
|
||||
#include "thermometer.h"
|
||||
#include "hygrometer.h"
|
||||
#include "co2_level.h"
|
||||
#include "battery_percent.h"
|
||||
|
||||
extern const int SLEEP_GRANULARITY; // [min]
|
||||
extern const int SLEEP_MIN_DURATION; // [min]
|
||||
extern const int SLEEP_MAX_DURATION; // [min]
|
||||
|
||||
/**
|
||||
* @brief thread function to run the supervisor in zephyr environment
|
||||
*/
|
||||
void thread_supervisor();
|
||||
|
||||
/**
|
||||
* @brief init the supervisor, and thus the complete plein_de_ee application
|
||||
* @return init_failed upon any error occurring during the initialisation of any module
|
||||
* @return success otherwise
|
||||
*/
|
||||
enum error_code supervisor_init();
|
||||
|
||||
/**
|
||||
* @brief exexcute the plein_de_eeeeeee application
|
||||
* @return error_unknown
|
||||
* @note should never return
|
||||
*/
|
||||
enum error_code supervisor_run();
|
||||
|
||||
#endif //SUPERVISOR_H
|
||||
23
nodes/src/thermometer.c
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "thermometer.h"
|
||||
|
||||
static const struct device* dev = DEVICE_DT_GET_ONE(st_hts221);
|
||||
|
||||
enum error_code thermo_init(){
|
||||
enum error_code ret = init_failed;
|
||||
if(device_is_ready(dev)){
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum error_code thermo_get_value(int* holder){
|
||||
enum error_code ret = read_failed;
|
||||
struct sensor_value temp;
|
||||
if( (sensor_sample_fetch(dev) >= 0) &&
|
||||
(sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp) >= 0)
|
||||
){
|
||||
*holder = sensor_value_to_deci(&temp);
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
26
nodes/src/thermometer.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef THERMOMETER_H
|
||||
#define THERMOMETER_H
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/sensor.h>
|
||||
|
||||
#include "error_code.h"
|
||||
|
||||
/**
|
||||
* @brief init the thermometer module
|
||||
* @return init_failed upon any error during init
|
||||
* @return success otherwise
|
||||
*/
|
||||
enum error_code thermo_init();
|
||||
|
||||
/**
|
||||
* @brief Retrieve the temperature and stores it in the given parameter
|
||||
* @param[out] holder : pointer where the measurement will be stored
|
||||
* @return read_failed upon any error occuring during measurement
|
||||
* @return success otherwise
|
||||
* @note the content of holder should not be trusted upon returning something else than success
|
||||
*/
|
||||
enum error_code thermo_get_value(int* holder);
|
||||
|
||||
#endif //THERMOMETER_H
|
||||
17
nodes/src/window_status.c
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "window_status.h"
|
||||
|
||||
static const struct gpio_dt_spec window_switch = GPIO_DT_SPEC_GET(DT_NODELABEL(window_switch), gpios);
|
||||
|
||||
enum error_code window_init(){
|
||||
enum error_code ret = init_failed;
|
||||
if(gpio_is_ready_dt(&window_switch) && (0 == gpio_pin_configure_dt(&window_switch, GPIO_INPUT))){
|
||||
ret = success;
|
||||
}else{}
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum error_code window_get_value(enum window_status* holder){
|
||||
// active high
|
||||
*holder = (gpio_pin_get_dt(&window_switch) ? closed : open);
|
||||
return success;
|
||||
}
|
||||
31
nodes/src/window_status.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifndef WINDOW_STATUS_H
|
||||
#define WINDOW_STATUS_H
|
||||
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
|
||||
#include "error_code.h"
|
||||
|
||||
enum window_status{
|
||||
closed = 0, // window is closed or sensor is not connected
|
||||
open, // window is opened
|
||||
unknown, // window opening status could not be read
|
||||
windows_status_last, // iteration purpose
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief init the window opening status module
|
||||
* @return init_failed upon any error during init
|
||||
* @return success otherwise
|
||||
*/
|
||||
enum error_code window_init();
|
||||
|
||||
/**
|
||||
* @brief Retrieve the window opening status and stores it in the given parameter
|
||||
* @param[out] holder : pointer where the measurement will be stored
|
||||
* @return read_failed upon any error occuring during measurement
|
||||
* @return success otherwise
|
||||
* @note the content of holder should not be trusted upon returning something else than success
|
||||
*/
|
||||
enum error_code window_get_value(enum window_status* holder);
|
||||
|
||||
#endif //WINDOW_STATUS_
|
||||
@@ -1,11 +1,11 @@
|
||||
#import "../../resources/helper.typ": *
|
||||
#let b = actor("broker",
|
||||
disp_name: [@mqtt\ broker],
|
||||
disp_name: [@mqtt:short\ broker],
|
||||
shape: "queue",
|
||||
show-bottom:false
|
||||
)
|
||||
#let mg = actor("mqtt",
|
||||
disp_name: [@mqtt\ gateway],
|
||||
disp_name: [@mqtt:short\ gateway],
|
||||
show-bottom:false
|
||||
)
|
||||
#let ig = actor("influx",
|
||||
|
||||
106
report/meetings/260611-final/db.typ
Normal file
@@ -0,0 +1,106 @@
|
||||
#import "/metadata.typ": *
|
||||
#import "/tail/bibliography.typ": *
|
||||
#import "/tail/glossary.typ": *
|
||||
#import "/main/architecture/description.typ": *
|
||||
|
||||
#import "/resources/slides.typ": *
|
||||
|
||||
#import "/main/database/sequence.typ": *
|
||||
// server
|
||||
// mqtt->db
|
||||
// db->rest
|
||||
|
||||
|
||||
== Database & API - Server
|
||||
#let server = [
|
||||
#figure(
|
||||
image("server.png", width: 100%),
|
||||
caption: [Server implementation],
|
||||
) <fig:server>
|
||||
]
|
||||
#grid(
|
||||
columns: (1.5fr, 1fr),
|
||||
column-gutter: 2em,
|
||||
server,
|
||||
align(top+left)[
|
||||
#v(3em) #pause
|
||||
- LXC Debian #pause
|
||||
- @ssh:short certificate by user #pause
|
||||
- Docker #pause
|
||||
- Traefik
|
||||
],
|
||||
)
|
||||
|
||||
== Database & API - Save measures in DB
|
||||
#let toDB = {
|
||||
import chronos: *
|
||||
b.display
|
||||
mg.display
|
||||
main.display
|
||||
ig.display
|
||||
db.display
|
||||
_col(b.name, mg.name, width: 5cm)
|
||||
_col(mg.name, main.name, width: 5cm)
|
||||
_col(main.name, ig.name, width: 5cm)
|
||||
_col(ig.name, db.name, width: 5cm)
|
||||
}
|
||||
|
||||
#let toDB-seq = (
|
||||
async(b, mg, "message"),
|
||||
sync(mg, main, "DataPoint"),
|
||||
sync(main, main, "map topics"),
|
||||
sync(main, ig, "DataPoint"),
|
||||
async(ig, db, "flush")
|
||||
)
|
||||
|
||||
#set align(center+top)
|
||||
#v(1em)
|
||||
#figure(
|
||||
box(height: 10cm, {
|
||||
let diag = toDB
|
||||
for (i, step) in toDB-seq.enumerate(start: 1) {
|
||||
diag += step
|
||||
only(i, chronos.diagram(diag, width: 90%))
|
||||
}
|
||||
}),
|
||||
caption: [Sequence from broker to DB]
|
||||
) <fig:seq:toDB>
|
||||
|
||||
|
||||
== Database & API - Get data from DB
|
||||
#let fromDB = {
|
||||
import chronos: *
|
||||
db.display
|
||||
ig.display
|
||||
main.display
|
||||
rg.display
|
||||
u.display
|
||||
_col(db.name, ig.name, width: 5cm)
|
||||
_col(ig.name, main.name, width: 5cm)
|
||||
_col(main.name, rg.name, width: 5cm)
|
||||
_col(rg.name, u.name, width: 5cm)
|
||||
}
|
||||
|
||||
#let fromDB-seq = (
|
||||
async(u, rg, "Request"),
|
||||
sync(rg, main, "getNodes"),
|
||||
sync(main, rg, "", dashed: true),
|
||||
sync(rg, ig, "Query"),
|
||||
sync(ig, db, ""),
|
||||
sync(db, ig, "", dashed: true),
|
||||
sync(ig, rg, "", dashed: true),
|
||||
sync(rg, u, "", dashed: true),
|
||||
)
|
||||
|
||||
#set align(center+top)
|
||||
#v(1em)
|
||||
#figure(
|
||||
box(height: 11.75cm, {
|
||||
let diag = fromDB
|
||||
for (i, step) in fromDB-seq.enumerate(start: 1) {
|
||||
diag += step
|
||||
only(i, chronos.diagram(diag, width: 90%))
|
||||
}
|
||||
}),
|
||||
caption: [Sequence to REST from DB]
|
||||
) <fig:seq:fromDB>
|
||||
67
report/meetings/260611-final/gateway.typ
Normal file
@@ -0,0 +1,67 @@
|
||||
#import "/metadata.typ": *
|
||||
#import "/tail/bibliography.typ": *
|
||||
#import "/tail/glossary.typ": *
|
||||
#import "/main/architecture/description.typ": *
|
||||
#import "/resources/slides.typ": *
|
||||
== Gateway — BLE to MQTT Bridge
|
||||
#slide[
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
gutter: 2em,
|
||||
align: top+left,
|
||||
[
|
||||
*Architecture*
|
||||
- Raspberry Pi 4 — Python
|
||||
- Passive @ble:short scan (bleak)
|
||||
- MQTTS publisher (paho-mqtt)
|
||||
- systemd service — auto-restart
|
||||
- Remote access via Tailscale
|
||||
],
|
||||
[
|
||||
// *Data flow*\
|
||||
// Thingy:52
|
||||
// $->$ BLE advertising
|
||||
// Raspberry Pi
|
||||
// $->$ MQTTS (TLS)
|
||||
// RabbitMQ broker
|
||||
// $->$
|
||||
// InfluxDB
|
||||
]
|
||||
)
|
||||
]
|
||||
== Gateway — Overview
|
||||
#slide[
|
||||
#align(center)[
|
||||
#figure(
|
||||
image("/resources/img/gateway_overview.svg", height: 40%),
|
||||
caption: [Gateway communication chain — from @ble:short advertising to database storage]
|
||||
)
|
||||
]
|
||||
]
|
||||
== Gateway — Key Challenges
|
||||
#slide[
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
gutter: 2em,
|
||||
align: top+left,
|
||||
[
|
||||
*BLE filtering*
|
||||
- GATT $->$ passive advertising
|
||||
- Filter: `company_id = 0xffff`
|
||||
- exactly 14 bytes
|
||||
- Continuous BLE scan (100% duty cycle)
|
||||
- Deduplication: 10s cache per MAC address
|
||||
],
|
||||
[
|
||||
*Reliability — validated in production*
|
||||
- MQTT auto-restart via systemd
|
||||
- `os._exit(1)` on disconnection
|
||||
- Validated with 2 cases:
|
||||
- Network disconnection
|
||||
- Wrong MQTT credentials
|
||||
- Multiple nodes publishing
|
||||
simultaneously — confirmed by broker
|
||||
- No duplicate data
|
||||
]
|
||||
)
|
||||
]
|
||||
60
report/meetings/260611-final/models.typ
Normal file
@@ -0,0 +1,60 @@
|
||||
#import "/metadata.typ": *
|
||||
#import "/tail/bibliography.typ": *
|
||||
#import "/tail/glossary.typ": *
|
||||
#import "/main/architecture/description.typ": *
|
||||
#import "/resources/slides.typ": *
|
||||
---
|
||||
Output:
|
||||
- Evolution of @co2:short concentration without considering air ventilation #pause
|
||||
- Time required to reach a threshold of 1400 ppm #pause
|
||||
- Evolution of @co2:short concentration decrease under natural ventilation #pause
|
||||
- Time required to reach outdoor-equivalent concentration level #pause
|
||||
|
||||
Input:
|
||||
- Provence classrooms specifications - Space A #pause
|
||||
- Open data #pause
|
||||
- Ventilation standard formula #pause
|
||||
- User parameters : Room name, number of students, initial @co2:short concentration
|
||||
|
||||
---
|
||||
=== Modelling the temporal evolution of @co2:short concentration
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
[
|
||||
#figure(
|
||||
image("../../resources/img/Physical model/CO2 concentration over time Simaria.png"),
|
||||
caption: [Comparison of modelled and open data #cite(<Simaria>) @co2:short concentration evolution]
|
||||
) <fig:comparison_open_data_model_no_window_opening>
|
||||
],
|
||||
[
|
||||
#figure(
|
||||
image("../../resources/img/Physical model/CO2 concentration over time INRS secondary school.png"),
|
||||
caption: [Comparison of modelled and open data #cite(<INRS_Study>) @co2:short concentration evolution]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
---
|
||||
=== Modelling the @co2:short concentration increase without air ventilation and @co2:short decrease under natural ventilation
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
[
|
||||
#figure(
|
||||
image("../../resources/img/Physical model/Window user window opening model.png"),
|
||||
caption: [User input parameters and results]
|
||||
) <fig:user_input_parameters_results>
|
||||
],pause,
|
||||
[
|
||||
#figure(
|
||||
image("../../resources/img/Physical model/CO2 concentration over time user window opening model.png"),
|
||||
caption: [Temporal evolution of @co2:short level using input parameters from @fig:user_input_parameters_results]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
---
|
||||
=== Comparison of experimental data and the physical model
|
||||
#figure(
|
||||
image("../../resources/img/Physical model/Comparison_expdata_model.png", width: 59%),
|
||||
caption: [Comparison of experimental data and the physical model (A2 classroom, 11 students)]
|
||||
)
|
||||
45
report/meetings/260611-final/nodes.typ
Normal file
@@ -0,0 +1,45 @@
|
||||
#import "/metadata.typ": *
|
||||
#import "/tail/bibliography.typ": *
|
||||
#import "/tail/glossary.typ": *
|
||||
#import "/main/architecture/description.typ": *
|
||||
|
||||
#import "/resources/slides.typ": *
|
||||
---
|
||||
== Nodes | Class diagram
|
||||
#let nodes_class_diagram_impl = [
|
||||
#figure(
|
||||
image("../..//resources/img/nodes_class_diagram_impl.svg"),
|
||||
caption: [Nodes class diagram following implementation]
|
||||
) <fig:nodes_class_diagram_impl>
|
||||
]
|
||||
#nodes_class_diagram_impl
|
||||
== Nodes | Sequence diagram
|
||||
#let nodes_sequence_diagram = [
|
||||
#figure(
|
||||
image("../../resources/img/nodes_sequence_diagram.svg", width: 60%),
|
||||
caption: [Nodes sequence diagram]
|
||||
) <fig:nodes_sequence_diagram>
|
||||
]
|
||||
#nodes_sequence_diagram
|
||||
== Nodes | BLE data
|
||||
#figure(
|
||||
table(
|
||||
columns: (auto, auto, auto),
|
||||
align: center,
|
||||
table.header("name", "key", "data size"),
|
||||
[Window opening status],[0x01],[1B],
|
||||
[Humidity],[0x02],[1B],
|
||||
[Temperature],[0x03],[2B],
|
||||
[@co2:short level],[0x04],[4B],
|
||||
[Battery percent of charge],[0x05],[1B],
|
||||
),
|
||||
caption: [Data communicated in the nodes_interface],
|
||||
)<tab:nodes_interface_content>
|
||||
|
||||
== Nodes | Takeaways
|
||||
#align(top+left)[
|
||||
#v(5em)
|
||||
- Breadboard validation #pause
|
||||
- 28 days later #pause
|
||||
- Improve @ble:short reliability
|
||||
]
|
||||
BIN
report/meetings/260611-final/server.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
13
report/meetings/260611-final/server.uxf
Normal file
@@ -0,0 +1,13 @@
|
||||
<diagram program="umletino" version="15.1"><zoom_level>10</zoom_level><element><id>UMLDeployment</id><coordinates><x>310</x><y>280</y><w>330</w><h>350</h></coordinates><panel_attributes>Server</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLGeneric</id><coordinates><x>340</x><y>390</y><w>100</w><h>40</h></coordinates><panel_attributes><<Broker MQTT>>
|
||||
RabbitMQ</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLGeneric</id><coordinates><x>490</x><y>320</y><w>100</w><h>40</h></coordinates><panel_attributes><<Database>>
|
||||
InfluxDB3</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLGeneric</id><coordinates><x>490</x><y>390</y><w>100</w><h>40</h></coordinates><panel_attributes><<API DB>>
|
||||
Go service</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>430</x><y>400</y><w>80</w><h>30</h></coordinates><panel_attributes>lt=<-</panel_attributes><additional_attributes>60;10;10;10</additional_attributes></element><element><id>Relation</id><coordinates><x>560</x><y>350</y><w>30</w><h>60</h></coordinates><panel_attributes>lt=-></panel_attributes><additional_attributes>10;10;10;40</additional_attributes></element><element><id>Relation</id><coordinates><x>510</x><y>350</y><w>30</w><h>60</h></coordinates><panel_attributes>lt=-></panel_attributes><additional_attributes>10;40;10;10</additional_attributes></element><element><id>Relation</id><coordinates><x>230</x><y>390</y><w>130</w><h>50</h></coordinates><panel_attributes>lt=-()
|
||||
m2=MQTT
|
||||
</panel_attributes><additional_attributes>110;20;10;20</additional_attributes></element><element><id>Relation</id><coordinates><x>530</x><y>430</y><w>190</w><h>50</h></coordinates><panel_attributes>lt=-()
|
||||
m2=REST
|
||||
</panel_attributes><additional_attributes>10;20;170;20</additional_attributes></element><element><id>UMLDeployment</id><coordinates><x>330</x><y>510</y><w>110</w><h>50</h></coordinates><panel_attributes><<Dashboard>>
|
||||
Angular</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>430</x><y>490</y><w>140</w><h>60</h></coordinates><panel_attributes>lt=)-
|
||||
m1=
|
||||
</panel_attributes><additional_attributes>110;20;110;40;10;40</additional_attributes></element><element><id>UMLDeployment</id><coordinates><x>330</x><y>570</y><w>110</w><h>50</h></coordinates><panel_attributes><<Notification>>
|
||||
Java</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>430</x><y>520</y><w>130</w><h>90</h></coordinates><panel_attributes></panel_attributes><additional_attributes>110;10;110;70;10;70</additional_attributes></element><element><id>Relation</id><coordinates><x>530</x><y>420</y><w>60</w><h>90</h></coordinates><panel_attributes>lt=-()
|
||||
m2=REST</panel_attributes><additional_attributes>10;10;10;70</additional_attributes></element></diagram>
|
||||
@@ -1,14 +1,14 @@
|
||||
#import "/metadata.typ": *
|
||||
#import "/tail/bibliography.typ": *
|
||||
#import "/tail/glossary.typ": *
|
||||
#import "/main/architecture/description.typ": *
|
||||
|
||||
#import "/resources/slides.typ": *
|
||||
|
||||
#show:make-glossary
|
||||
#register-glossary(entry-list)
|
||||
|
||||
#let HANDOUT = true
|
||||
#let NOTES = false
|
||||
#let HANDOUT = false
|
||||
#let NOTES = true
|
||||
|
||||
#show: metropolis-theme.with(
|
||||
aspect-ratio: "16-9",
|
||||
@@ -33,7 +33,7 @@
|
||||
#title-slide()
|
||||
|
||||
// 20 min presentation
|
||||
// 5 (students) + 25 (teacher) min Q&A
|
||||
// 5 (students) + 25 (teachers) min Q&A
|
||||
|
||||
/*
|
||||
technical -> each section should go for around (3min / pers)
|
||||
@@ -58,16 +58,138 @@ TECHNIQUE
|
||||
|
||||
*/
|
||||
|
||||
= Intro
|
||||
= Intro // (50s) Rémi
|
||||
// Context of the project
|
||||
// Dimitri missing
|
||||
#speaker-note[
|
||||
This is a personal note
|
||||
]
|
||||
---
|
||||
|
||||
== foo
|
||||
|
||||
Yolo
|
||||
== Architecture // (50s) Ibrahima
|
||||
#let top_level_architecture = [
|
||||
#figure(
|
||||
image("../../resources/img/ui_images/architecture.png"),
|
||||
caption: [Top level architecture]
|
||||
) <fig:top_level_architecture>
|
||||
]
|
||||
#top_level_architecture
|
||||
|
||||
== bar
|
||||
Hello world
|
||||
== Organisation & Task Management // (50s) Djelal
|
||||
|
||||
#slide[
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
gutter: 2em,
|
||||
align: left+top,
|
||||
[
|
||||
*Project management*
|
||||
- Weekly meetings
|
||||
- PV after each meeting
|
||||
- GitHub Issues & sub-issues
|
||||
- Pull Requests with code review
|
||||
- Teams for daily communication
|
||||
- GULAG Git conventions
|
||||
],
|
||||
[
|
||||
*Work distribution*
|
||||
- Adrien — Nodes firmware (Zephyr)
|
||||
- Djelal — Gateway (BLE-to-MQTT)
|
||||
- Rémi — Database & API
|
||||
- Ibrahima — User Interface
|
||||
- Alison — Physical model
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
= Nodes // (3min) Adrien
|
||||
#include "nodes.typ"
|
||||
|
||||
= Gateway // (3min) Djelal
|
||||
#include "gateway.typ"
|
||||
|
||||
= Database & API // (3min) Rémi
|
||||
#include "db.typ"
|
||||
|
||||
= User interface // (3min) Ibrahima
|
||||
#include "ui.typ"
|
||||
|
||||
= Physical model // (3min) Alison
|
||||
#include "models.typ"
|
||||
|
||||
= Conclusion
|
||||
|
||||
---
|
||||
It's the end of the world
|
||||
== Project's takeaways // (50s) Adrien (Regard critique)
|
||||
- @trl:long (@trl:short) 4 #pause
|
||||
- Forecasting // and Teams notifications
|
||||
|
||||
== Future perspectives // (50s) Alison
|
||||
- Deployment in every room #pause
|
||||
- Equip the door with sensors #pause
|
||||
- Calibrate the sensors and, if necessary, replace them with higher-performance devices #pause
|
||||
- Conduct multiple measurement campaigns knowing the number of students to adjust the physical model #pause
|
||||
- Display the predicted time to reach the threshold and the window opening duration on the board #pause
|
||||
- Teams notifications #pause
|
||||
- Implement a forecasting using machine learning
|
||||
|
||||
|
||||
#focus-slide[Questions?]
|
||||
|
||||
#show: appendix
|
||||
|
||||
== Glossary
|
||||
#print-glossary(
|
||||
entry-list,
|
||||
// show all term even if they are not referenced, default to true
|
||||
show-all: false,
|
||||
// disable the back ref at the end of the descriptions
|
||||
disable-back-references: true
|
||||
)
|
||||
|
||||
|
||||
== Bibliography
|
||||
|
||||
#bibliography(title: i18n("bib-title", lang: option.lang), bib.path, style:bib.style)
|
||||
|
||||
= Annexes
|
||||
|
||||
== Description of the model
|
||||
|
||||
#grid(
|
||||
columns: (1.7fr,0.1fr,1fr),
|
||||
[#figure(
|
||||
image("../../resources/img/Physical model/data flow diagram window opening .png"),
|
||||
caption: [Description of the model]
|
||||
) <fig:physical_model_no_ventilation>],
|
||||
[
|
||||
],
|
||||
[
|
||||
Formulas for determining the evolution of @co2 concentration :
|
||||
|
||||
#text(size: 12pt)[
|
||||
- $C_"CO2" (t) = C_"CO2" (t=0) + frac(N.Q_"CO2_prod".t,V)
|
||||
$
|
||||
|
||||
- $C_"CO2" (t) = (C_"CO2_indoor" (t=0) - C_"CO2_outdoor" - frac(0.001 . Q_"CO2_prod", Q_"air")) . \ exp (frac(-Q_"air", V) . t)
|
||||
+ C_"CO2_outdoor" + frac(0.001 . Q_"CO2_prod", Q_"air")
|
||||
$
|
||||
where,
|
||||
|
||||
$C_"CO2" (t=0)$ #h(1.5cm) initial co2 concentration [ppm]
|
||||
|
||||
N #h(3.4cm) number of students
|
||||
|
||||
$Q_"CO2_prod"$ #h(2cm) co2 flow rate per person [l/h]
|
||||
|
||||
$C_"CO2_indoor" (t=0)$ #h(0.6cm) indoor co2 level before window-opening [ppm]
|
||||
|
||||
$C_"CO2_outdoor"$ #h(1.7cm) outdoor air concentration [ppm]
|
||||
|
||||
$Q_"air"$ #h(3cm) incoming air flow rate [$m^3$/h]
|
||||
|
||||
$V$ #h(3.3cm) room volume [$m^3$]
|
||||
|
||||
$t$ #h(3.5cm) time [h]]
|
||||
]
|
||||
)
|
||||
197
report/meetings/260611-final/ui.typ
Normal file
@@ -0,0 +1,197 @@
|
||||
#import "/metadata.typ": *
|
||||
#import "/tail/bibliography.typ": *
|
||||
#import "/tail/glossary.typ": *
|
||||
#import "/main/architecture/description.typ": *
|
||||
|
||||
#import "/resources/slides.typ": *
|
||||
|
||||
// Chemin racine des images — adapter selon ta structure de projet
|
||||
#let img-root = "../../resources/img/ui_images/images/"
|
||||
|
||||
// ── Palette ───────────────────────────────────────────────────────────────────
|
||||
#let c-dark = rgb("#0F172A")
|
||||
#let c-teal = rgb("#0EA5E9")
|
||||
#let c-text = rgb("#1E293B")
|
||||
#let c-muted = rgb("#64748B")
|
||||
#let c-white = rgb("#FFFFFF")
|
||||
#let c-border = rgb("#E2E8F0")
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
#let icon-circle(img-path, size: 38pt, bg: rgb("#0EA5E9")) = {
|
||||
box(
|
||||
width: size, height: size,
|
||||
fill: bg, radius: (size / 2),
|
||||
inset: 0pt, clip: true,
|
||||
)[
|
||||
#align(center + horizon)[
|
||||
#image(img-path, width: (size * 0.62), height: (size * 0.62), fit: "contain")
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
#let badge(label, fill: rgb("#EF4444")) = {
|
||||
box(fill: fill, radius: 3pt, inset: (x: 5pt, y: 2pt))[
|
||||
#text(size: 6.5pt, weight: "bold", fill: rgb("#FFFFFF"))[#label]
|
||||
]
|
||||
}
|
||||
|
||||
#let devsec-col(phase-title, items) = {
|
||||
block(width: 100%)[
|
||||
#block(
|
||||
width: 100%, height: 28pt,
|
||||
fill: c-dark,
|
||||
radius: (top-left: 5pt, top-right: 5pt, bottom-left: 0pt, bottom-right: 0pt),
|
||||
inset: 0pt,
|
||||
)[
|
||||
#pad(x: 7pt)[
|
||||
#align(horizon + center)[
|
||||
#text(size: 8.5pt, weight: "bold", fill: rgb("#FFFFFF"))[#phase-title]
|
||||
]
|
||||
]
|
||||
]
|
||||
#block(
|
||||
width: 100%,
|
||||
fill: rgb("#F1F5F9"),
|
||||
radius: (top-left: 0pt, top-right: 0pt, bottom-left: 5pt, bottom-right: 5pt),
|
||||
inset: 8pt,
|
||||
)[
|
||||
#for item in items {
|
||||
grid(columns: (30pt, 1fr), gutter: 6pt,
|
||||
icon-circle(item.at("icon"), size: 28pt, bg: item.at("bg", default: c-teal)),
|
||||
align(left + horizon)[
|
||||
#text(size: 8pt, weight: "bold", fill: c-text)[#item.at("name")]
|
||||
#linebreak()
|
||||
#item.at("extra", default: [])
|
||||
],
|
||||
)
|
||||
v(5pt)
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
== Cycle DevSecOps
|
||||
|
||||
#slide[
|
||||
#grid(columns: (1fr, 1fr, 1fr, 1fr), gutter: 8pt,
|
||||
devsec-col("① Code & PR Gate", (
|
||||
(icon: img-root + "image10.png", bg: rgb("#3178C6"), name: "TypeScript",
|
||||
extra: [#text(size: 6.5pt, fill: c-muted)[tsc / ESLint]]),
|
||||
(icon: img-root + "image11.png", bg: rgb("#DD0031"), name: "Angular CI",
|
||||
extra: [#text(size: 6.5pt, fill: c-muted)[Build check]]),
|
||||
(icon: img-root + "image19.png", bg: rgb("#1F2328"), name: "GitHub Actions",
|
||||
extra: [#text(size: 6.5pt, fill: c-muted)[Coverage]]),
|
||||
)),pause,
|
||||
devsec-col("② SAST · SCA", (
|
||||
(icon: img-root + "image7.png", bg: rgb("#000000"), name: "SpotBugs",
|
||||
extra: [#badge("BLOCKING")]),
|
||||
(icon: img-root + "image13.png", bg: rgb("#1F2328"), name: "CodeQL",
|
||||
extra: [#badge("BLOCKING")]),
|
||||
(icon: img-root + "image14.png", bg: rgb("#F97316"), name: "Dep. Check",
|
||||
extra: [#badge("NON-BLOCK", fill: rgb("#F97316"))]),
|
||||
)),pause,
|
||||
devsec-col("③ DAST · Tests", (
|
||||
(icon: img-root + "image15.png", bg: rgb("#00549E"), name: "OWASP ZAP",
|
||||
extra: [#badge("BLOCKING")]),
|
||||
(icon: img-root + "image16.png", bg: rgb("#DD0031"), name: "Karma Tests",
|
||||
extra: [#badge("BLOCKING")]),
|
||||
(icon: img-root + "image17.png", bg: rgb("#64748B"), name: "Runtime check",
|
||||
extra: [#text(size: 6.5pt, fill: c-muted)[HTTP headers]]),
|
||||
)),pause,
|
||||
devsec-col("④ Build · Deploy", (
|
||||
(icon: img-root + "image18.png", bg: rgb("#0DB7ED"), name: "Docker",
|
||||
extra: [#text(size: 6.5pt, fill: c-muted)[SHA-tagged]]),
|
||||
(icon: img-root + "image19.png", bg: rgb("#1F2328"), name: "GHCR Push",
|
||||
extra: [#text(size: 6.5pt, fill: c-muted)[main only]]),
|
||||
(icon: img-root + "image17.png", bg: rgb("#10B981"), name: "SSH Deploy",
|
||||
extra: [#text(size: 6.5pt, fill: c-muted)[cert-auth]]),
|
||||
)),pause,
|
||||
)
|
||||
|
||||
#v(7pt)
|
||||
|
||||
#rect(width: 100%, height: 26pt, fill: c-dark, radius: 4pt, inset: 0pt)[
|
||||
#pad(x: 20pt)[
|
||||
#align(horizon)[
|
||||
#grid(columns: (1fr, 1fr, 1fr),
|
||||
align(center + horizon)[#text(size: 8pt, fill: rgb("#FFFFFF"))[🔐 #h(2pt) Shift-left security]],
|
||||
align(center + horizon)[#text(size: 8pt, fill: rgb("#FFFFFF"))[🔑 #h(2pt) Zero secret in code]],
|
||||
align(center + horizon)[#text(size: 8pt, fill: rgb("#FFFFFF"))[🔄 #h(2pt) Automated Deployment]],
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
#v(6pt)
|
||||
]
|
||||
|
||||
|
||||
// ── SLIDE 3 — Dashboard ───────────────────────────────────────────────────────
|
||||
== Dashboard
|
||||
|
||||
#slide[
|
||||
#align(center + horizon)[
|
||||
#figure(
|
||||
image(img-root + "image20.png", width: 100%, fit: "contain"),
|
||||
caption: [Dashboard]
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
// ── SLIDE 4 — Details Page ────────────────────────────────────────────────────
|
||||
== Details Page
|
||||
|
||||
#slide[
|
||||
#align(center + horizon)[
|
||||
#figure(
|
||||
image(img-root + "image21.png", width: 100%, fit: "contain"),
|
||||
caption: [Details page]
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
// ── SLIDE 5 — Notification ────────────────────────────────────────────────────
|
||||
== Notification
|
||||
|
||||
#slide[
|
||||
#grid(columns: (1fr, 1fr), gutter: 16pt,
|
||||
// Screenshot Telegram
|
||||
align(center + horizon)[
|
||||
|
||||
#figure(
|
||||
image(img-root + "image22.png", height: 300pt, fit: "contain"),
|
||||
caption: [Telegram notification]
|
||||
)
|
||||
],
|
||||
// Carte descriptive
|
||||
rect(width: 100%, radius: 6pt, stroke: 0.5pt + c-border, fill: rgb("#FFFFFF"), inset: 12pt)[
|
||||
#text(size: 10pt, weight: "bold", fill: c-text)[CO₂ Alerts System]
|
||||
#v(5pt)
|
||||
#line(length: 100%, stroke: 0.5pt + c-border)
|
||||
#v(6pt)
|
||||
|
||||
#let alert-row(col, level, desc) = {
|
||||
grid(columns: (10pt, 52pt, 1fr), gutter: 5pt,
|
||||
box(width: 8pt, height: 8pt, fill: col, radius: 4pt),
|
||||
text(size: 8.5pt, weight: "bold")[#level],
|
||||
text(size: 8pt, fill: c-muted)[#desc],
|
||||
)
|
||||
v(4pt)
|
||||
}
|
||||
#alert-row(rgb("#f44336"), "Critical", "> 2000 ppm")
|
||||
#alert-row(rgb("#F97316"), "Very Poor", "1500-2000 ppm")
|
||||
#alert-row(rgb("#ff9800"), "Poor", "1200-1500 ppm")
|
||||
#alert-row(rgb("#ffc107"), "Moderate", "1000-1200 ppm")
|
||||
#alert-row(rgb("#8bc34a"), "Good", "800-1000 ppm")
|
||||
#alert-row(rgb("#4caf50"), "Excellent", "< 800 ppm")
|
||||
|
||||
#v(6pt)
|
||||
#line(length: 100%, stroke: 0.5pt + c-border)
|
||||
#v(6pt)
|
||||
|
||||
#v(6pt)
|
||||
|
||||
],
|
||||
)
|
||||
]
|
||||
43
report/resources/img/gateway_overview.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="238px" preserveAspectRatio="none" style="width:1106px;height:238px;" version="1.1" viewBox="0 0 1106 238" width="1106px" zoomAndPan="magnify"><defs><filter height="300%" id="fdgkvirjllmq" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><!--MD5=[d638d2e8639369a4c8d274dd926c65e9]
|
||||
entity thingy--><rect fill="#ADD8E6" filter="url(#fdgkvirjllmq)" height="117.7813" style="stroke: #A80036; stroke-width: 1.5;" width="100" x="6" y="11.5"/><rect fill="#ADD8E6" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="1" y="16.5"/><rect fill="#ADD8E6" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="1" y="119.2813"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="69" x="16" y="34.4951">Thingy:52</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="80" x="16" y="50.792">──────────</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="78" x="16" y="67.0889">CO2, Temp</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="59" x="16" y="83.3857">Humidity</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="49" x="16" y="99.6826">Battery</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="54" x="16" y="115.9795">Window</text><!--MD5=[7b30460008fc71f9986330c33ee2d290]
|
||||
entity gateway--><rect fill="#90EE90" filter="url(#fdgkvirjllmq)" height="101.4844" style="stroke: #A80036; stroke-width: 1.5;" width="151" x="310" y="20"/><rect fill="#90EE90" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="305" y="25"/><rect fill="#90EE90" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="305" y="111.4844"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="99" x="320" y="42.9951">Raspberry Pi 4</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="80" x="320" y="59.292">──────────</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="77" x="320" y="75.5889">gateway.py</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="129" x="320" y="91.8857">bleak + paho-mqtt</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="131" x="320" y="108.1826">asyncio + systemd</text><!--MD5=[e701bbac852ce3c7f1c3d2193e52d58f]
|
||||
entity broker--><rect fill="#FFFFE0" filter="url(#fdgkvirjllmq)" height="85.1875" style="stroke: #A80036; stroke-width: 1.5;" width="119" x="710" y="28"/><rect fill="#FFFFE0" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="705" y="33"/><rect fill="#FFFFE0" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="705" y="103.1875"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="67" x="720" y="50.9951">RabbitMQ</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="80" x="720" y="67.292">──────────</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="99" x="720" y="83.5889">MQTTS broker</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="76" x="720" y="99.8857">TLS + auth</text><!--MD5=[7468467f3b6b392f1d176d828c753b77]
|
||||
entity db--><rect fill="#FFA07A" filter="url(#fdgkvirjllmq)" height="36.2969" style="stroke: #A80036; stroke-width: 1.5;" width="76" x="1019" y="52.5"/><rect fill="#FFA07A" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="1014" y="57.5"/><rect fill="#FFA07A" height="5" style="stroke: #A80036; stroke-width: 1.5;" width="10" x="1014" y="78.7969"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="56" x="1029" y="75.4951">InfluxDB</text><path d="M307,156 L307,226.5313 A0,0 0 0 0 307,226.5313 L464,226.5313 A0,0 0 0 0 464,226.5313 L464,166 L454,156 L389.5,156 L385.5,121.02 L381.5,156 L307,156 A0,0 0 0 0 307,156 " fill="#FBFB77" filter="url(#fdgkvirjllmq)" style="stroke: #A80036; stroke-width: 1.0;"/><path d="M454,156 L454,166 L464,166 L454,156 " fill="#FBFB77" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="42" x="313" y="173.0669">Filters:</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="136" x="313" y="188.1997">- company_id = 0xffff</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="133" x="313" y="203.3325">- payload = 14 bytes</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="134" x="313" y="218.4653">- dedup 10s per MAC</text><!--MD5=[72a710db11e8fbeffe0299f3833be780]
|
||||
link thingy to gateway--><path d="M106.03,70.5 C158.53,70.5 242.65,70.5 304.72,70.5 " fill="none" id="thingy->gateway" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="309.9,70.5,300.9,66.5,304.9,70.5,300.9,74.5,309.9,70.5" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="98" x="157.5" y="21.5669">BLE advertising</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="112" x="150.5" y="36.6997">company_id 0xffff</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="55" x="179" y="51.8325">14 bytes</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="139" x="137" y="66.9653">3 channels (37/38/39)</text><!--MD5=[d5fc4fe9659f4c010806cb130a744287]
|
||||
link gateway to broker--><path d="M461.06,70.5 C531.95,70.5 637.45,70.5 704.59,70.5 " fill="none" id="gateway->broker" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="709.76,70.5,700.76,66.5,704.76,70.5,700.76,74.5,709.76,70.5" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="94" x="540" y="36.5669">MQTTS port 80</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="86" x="544" y="51.6997">JSON payload</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="184" x="495" y="66.8325">{gateway_id}/{mac}/update</text><!--MD5=[b33725c11cd6fb0cd801e7a81b9a0575]
|
||||
link broker to db--><path d="M829.31,70.5 C884.12,70.5 964.53,70.5 1013.4,70.5 " fill="none" id="broker->db" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="1018.61,70.5,1009.61,66.5,1013.61,70.5,1009.61,74.5,1018.61,70.5" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="128" x="860" y="66.5669">store measurement</text><!--MD5=[9109db2b88df5a8c8bdd4041f3fe8ffc]
|
||||
@startuml
|
||||
skinparam linestyle ortho
|
||||
skinparam componentStyle rectangle
|
||||
skinparam backgroundColor white
|
||||
|
||||
left to right direction
|
||||
|
||||
component "Thingy:52\n──────────\nCO2, Temp\nHumidity\nBattery\nWindow" as thingy #LightBlue
|
||||
component "Raspberry Pi 4\n──────────\ngateway.py\nbleak + paho-mqtt\nasyncio + systemd" as gateway #LightGreen
|
||||
component "RabbitMQ\n──────────\nMQTTS broker\nTLS + auth" as broker #LightYellow
|
||||
component "InfluxDB" as db #LightSalmon
|
||||
|
||||
thingy - -> gateway : BLE advertising\ncompany_id 0xffff\n14 bytes\n3 channels (37/38/39)
|
||||
gateway - -> broker : MQTTS port 80\nJSON payload\n{gateway_id}/{mac}/update
|
||||
broker - -> db : store measurement
|
||||
|
||||
note bottom of gateway
|
||||
Filters:
|
||||
- company_id = 0xffff
|
||||
- payload = 14 bytes
|
||||
- dedup 10s per MAC
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
||||
PlantUML version 1.2020.02(Sun Mar 01 11:22:07 CET 2020)
|
||||
(GPL source distribution)
|
||||
Java Runtime: OpenJDK Runtime Environment
|
||||
JVM: OpenJDK 64-Bit Server VM
|
||||
Java Version: 21.0.11+10-1-deb13u2-Debian
|
||||
Operating System: Linux
|
||||
Default Encoding: UTF-8
|
||||
Language: en
|
||||
Country: GB
|
||||
--></g></svg>
|
||||
|
After Width: | Height: | Size: 9.0 KiB |
BIN
report/resources/img/ui_images/architecture.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
report/resources/img/ui_images/image.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
report/resources/img/ui_images/images/image1.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
report/resources/img/ui_images/images/image10.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
report/resources/img/ui_images/images/image11.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
report/resources/img/ui_images/images/image12.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
report/resources/img/ui_images/images/image13.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
report/resources/img/ui_images/images/image14.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
report/resources/img/ui_images/images/image15.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
report/resources/img/ui_images/images/image16.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
report/resources/img/ui_images/images/image17.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
report/resources/img/ui_images/images/image18.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
report/resources/img/ui_images/images/image19.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
report/resources/img/ui_images/images/image2.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
report/resources/img/ui_images/images/image20.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
report/resources/img/ui_images/images/image21.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
report/resources/img/ui_images/images/image22.png
Normal file
|
After Width: | Height: | Size: 638 KiB |
BIN
report/resources/img/ui_images/images/image3.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
report/resources/img/ui_images/images/image4.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
report/resources/img/ui_images/images/image5.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
report/resources/img/ui_images/images/image6.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
report/resources/img/ui_images/images/image7.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
report/resources/img/ui_images/images/image8.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
report/resources/img/ui_images/images/image9.jpeg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
441
report/resources/measures/A3-D5_2F_7E_30_10_5A.csv
Normal file
@@ -0,0 +1,441 @@
|
||||
time,co2,temperature,humidity,windows,battery
|
||||
2026-06-11T08:45:04,1766,27.7,38,0,25
|
||||
2026-06-11T08:46:04,1815,28.1,37,0,27
|
||||
2026-06-11T08:48:05,1836,28.7,37,0,25
|
||||
2026-06-11T08:49:06,1844,29,36,0,25
|
||||
2026-06-11T08:50:06,1830,29.2,36,0,25
|
||||
2026-06-11T08:51:07,1837,29.5,35,0,27
|
||||
2026-06-11T08:53:08,1903,29.8,35,0,27
|
||||
2026-06-11T08:54:08,1903,30,34,0,25
|
||||
2026-06-11T08:55:09,1868,30.2,34,0,27
|
||||
2026-06-11T08:56:10,1900,30.3,34,0,25
|
||||
2026-06-11T08:57:10,1914,30.5,34,0,27
|
||||
2026-06-11T08:58:10,1925,30.6,33,0,27
|
||||
2026-06-11T08:59:11,1951,30.7,33,0,25
|
||||
2026-06-11T09:00:11,1975,30.8,33,0,27
|
||||
2026-06-11T09:01:12,1975,31,32,0,27
|
||||
2026-06-11T09:02:13,1942,31,33,0,25
|
||||
2026-06-11T09:03:13,1938,31.1,32,0,27
|
||||
2026-06-11T09:04:14,1965,31.2,32,0,27
|
||||
2026-06-11T09:05:14,1989,31.3,32,0,25
|
||||
2026-06-11T09:07:15,1978,31.5,32,0,27
|
||||
2026-06-11T09:08:15,1989,31.6,32,0,27
|
||||
2026-06-11T09:09:16,1989,31.6,31,0,27
|
||||
2026-06-11T09:10:16,2007,31.6,32,0,25
|
||||
2026-06-11T09:11:17,1978,31.3,32,0,27
|
||||
2026-06-11T09:12:18,1989,31.1,32,0,25
|
||||
2026-06-11T09:13:18,1965,30.7,32,0,25
|
||||
2026-06-11T09:14:18,1980,30.3,32,0,25
|
||||
2026-06-11T09:15:19,1946,30,32,0,25
|
||||
2026-06-11T09:16:19,1914,29.6,33,0,25
|
||||
2026-06-11T09:18:21,1926,29.1,33,0,25
|
||||
2026-06-11T09:19:21,1895,28.7,33,0,25
|
||||
2026-06-11T09:20:21,1937,28.6,33,0,25
|
||||
2026-06-11T09:21:22,1937,28.3,33,0,25
|
||||
2026-06-11T09:22:22,1937,28.2,34,0,27
|
||||
2026-06-11T09:23:23,1902,28,34,0,25
|
||||
2026-06-11T09:24:23,1890,27.8,34,0,25
|
||||
2026-06-11T09:25:24,1939,27.7,34,0,27
|
||||
2026-06-11T09:26:24,1960,27.6,35,0,25
|
||||
2026-06-11T09:27:25,1934,27.5,34,0,25
|
||||
2026-06-11T09:28:25,1933,27.3,35,0,25
|
||||
2026-06-11T09:29:26,1917,27.2,35,0,25
|
||||
2026-06-11T09:30:27,1908,27.2,35,0,25
|
||||
2026-06-11T09:31:27,1897,27.1,35,0,25
|
||||
2026-06-11T09:32:28,1879,27,35,0,25
|
||||
2026-06-11T09:33:28,1879,26.8,35,0,25
|
||||
2026-06-11T09:34:29,1900,26.7,35,0,25
|
||||
2026-06-11T09:35:29,1900,26.7,36,0,25
|
||||
2026-06-11T09:36:29,1885,26.6,36,0,25
|
||||
2026-06-11T09:37:30,1894,26.6,36,0,25
|
||||
2026-06-11T09:38:30,1885,26.5,36,0,27
|
||||
2026-06-11T09:39:31,1894,26.5,36,0,25
|
||||
2026-06-11T09:40:31,1885,26.5,36,0,27
|
||||
2026-06-11T09:41:32,1885,26.3,36,0,27
|
||||
2026-06-11T09:42:32,1875,26.3,36,0,27
|
||||
2026-06-11T09:43:33,1875,26.3,36,0,25
|
||||
2026-06-11T09:44:34,1902,26.2,37,0,27
|
||||
2026-06-11T09:45:34,1852,26.2,37,0,27
|
||||
2026-06-11T09:46:35,1852,26.2,37,0,25
|
||||
2026-06-11T09:47:35,1834,26.1,37,0,27
|
||||
2026-06-11T09:48:35,1843,26.1,37,0,25
|
||||
2026-06-11T09:49:36,1852,26.1,37,0,25
|
||||
2026-06-11T09:50:37,1843,26.1,37,0,25
|
||||
2026-06-11T09:51:37,1834,26,37,0,25
|
||||
2026-06-11T09:52:37,1825,26,37,0,25
|
||||
2026-06-11T09:53:38,1825,26,38,0,25
|
||||
2026-06-11T09:54:38,1811,25.8,37,0,27
|
||||
2026-06-11T09:55:39,1897,26,37,0,25
|
||||
2026-06-11T09:56:40,1819,25.8,38,0,25
|
||||
2026-06-11T09:57:40,1875,25.8,38,0,25
|
||||
2026-06-11T09:58:41,1855,25.8,38,0,25
|
||||
2026-06-11T09:59:41,1865,25.8,38,0,25
|
||||
2026-06-11T10:00:41,1847,25.7,38,0,27
|
||||
2026-06-11T10:01:42,1847,25.7,38,0,25
|
||||
2026-06-11T10:02:42,1847,25.7,38,0,25
|
||||
2026-06-11T10:03:43,1855,25.7,38,0,25
|
||||
2026-06-11T10:04:43,1855,25.7,39,0,25
|
||||
2026-06-11T10:05:44,1842,25.7,38,0,25
|
||||
2026-06-11T10:06:45,1855,25.7,39,0,25
|
||||
2026-06-11T10:07:45,1833,25.6,38,0,25
|
||||
2026-06-11T10:08:46,1837,25.6,39,0,25
|
||||
2026-06-11T10:09:46,1833,25.6,39,0,25
|
||||
2026-06-11T10:10:47,1833,25.5,39,0,25
|
||||
2026-06-11T10:11:47,1833,25.6,39,0,25
|
||||
2026-06-11T10:12:48,1833,25.3,41,0,25
|
||||
2026-06-11T10:13:48,1833,25.5,38,0,25
|
||||
2026-06-11T10:14:49,1847,25.5,39,0,25
|
||||
2026-06-11T10:15:49,1833,25.5,39,0,25
|
||||
2026-06-11T10:16:50,1833,25.3,39,0,25
|
||||
2026-06-11T10:17:50,1824,25.3,39,0,25
|
||||
2026-06-11T10:18:51,1824,25.3,40,0,25
|
||||
2026-06-11T10:19:51,1811,25.3,39,0,25
|
||||
2026-06-11T10:20:52,1860,25.5,39,0,25
|
||||
2026-06-11T10:21:52,1866,25.3,39,0,25
|
||||
2026-06-11T10:22:53,1897,25.5,39,0,25
|
||||
2026-06-11T10:23:53,1914,25.5,40,0,25
|
||||
2026-06-11T10:24:54,1915,25.5,40,0,25
|
||||
2026-06-11T10:25:54,1915,25.5,40,0,25
|
||||
2026-06-11T10:26:55,1935,25.5,40,0,25
|
||||
2026-06-11T10:27:55,1946,25.5,40,0,25
|
||||
2026-06-11T10:28:56,1956,25.5,39,0,25
|
||||
2026-06-11T10:29:56,1971,25.5,40,0,27
|
||||
2026-06-11T10:30:57,1956,25.5,40,0,25
|
||||
2026-06-11T10:31:57,1976,25.5,39,0,25
|
||||
2026-06-11T10:33:58,1965,25.5,40,0,25
|
||||
2026-06-11T10:34:59,1976,25.5,40,0,25
|
||||
2026-06-11T10:35:59,1965,25.5,40,0,25
|
||||
2026-06-11T10:37:00,1956,25.5,40,0,25
|
||||
2026-06-11T10:38:00,1956,25.5,39,0,25
|
||||
2026-06-11T10:39:01,1971,25.5,40,0,25
|
||||
2026-06-11T10:40:01,1935,25.3,40,0,25
|
||||
2026-06-11T10:41:02,1935,25.5,39,0,25
|
||||
2026-06-11T10:42:02,1950,25.3,39,0,25
|
||||
2026-06-11T10:43:03,1950,25.3,40,0,25
|
||||
2026-06-11T10:44:03,1915,25.3,40,0,25
|
||||
2026-06-11T10:45:04,1915,25.3,40,0,25
|
||||
2026-06-11T10:46:04,1915,25.3,40,0,25
|
||||
2026-06-11T10:47:05,1928,25.3,40,0,25
|
||||
2026-06-11T10:48:05,1928,25.3,40,0,25
|
||||
2026-06-11T10:49:06,1928,25.3,40,0,25
|
||||
2026-06-11T10:50:06,1928,25.3,40,0,25
|
||||
2026-06-11T10:51:07,1928,25.3,40,0,25
|
||||
2026-06-11T10:52:07,1915,25.2,40,0,25
|
||||
2026-06-11T10:53:08,1928,25.3,40,0,25
|
||||
2026-06-11T10:54:08,1928,25.3,40,0,25
|
||||
2026-06-11T10:55:09,1928,25.2,40,0,25
|
||||
2026-06-11T10:56:09,1928,25.2,40,0,25
|
||||
2026-06-11T10:57:10,1909,25.2,40,0,25
|
||||
2026-06-11T11:00:11,1899,25.2,39,0,25
|
||||
2026-06-11T11:01:12,1914,25.3,40,0,25
|
||||
2026-06-11T11:03:13,1899,25.2,39,0,25
|
||||
2026-06-11T11:04:13,1903,25.2,40,0,25
|
||||
2026-06-11T11:05:14,1899,25.2,40,0,25
|
||||
2026-06-11T11:06:14,1899,25.2,40,0,25
|
||||
2026-06-11T11:07:15,1899,25.2,39,0,25
|
||||
2026-06-11T11:08:15,1914,25.2,39,0,25
|
||||
2026-06-11T11:09:16,1914,25.2,39,0,25
|
||||
2026-06-11T11:10:16,1914,25.2,39,0,25
|
||||
2026-06-11T11:12:17,1922,25.2,40,0,25
|
||||
2026-06-11T11:13:18,1915,25.2,40,0,25
|
||||
2026-06-11T11:14:18,1915,25.2,40,0,25
|
||||
2026-06-11T11:15:19,1928,25.2,39,0,25
|
||||
2026-06-11T11:16:19,1940,25.2,40,0,25
|
||||
2026-06-11T11:17:20,1928,25.2,40,0,25
|
||||
2026-06-11T11:18:20,1915,25.2,40,0,25
|
||||
2026-06-11T11:19:21,1915,25.1,40,0,25
|
||||
2026-06-11T11:20:21,1909,25.2,39,0,25
|
||||
2026-06-11T11:21:22,1909,25.2,40,0,22
|
||||
2026-06-11T11:22:22,1899,25.2,39,0,25
|
||||
2026-06-11T11:23:23,1922,25.2,39,0,25
|
||||
2026-06-11T11:24:23,1922,25.2,39,0,25
|
||||
2026-06-11T11:26:24,1914,25.2,39,0,25
|
||||
2026-06-11T11:27:25,1903,25.2,40,0,25
|
||||
2026-06-11T11:28:26,1899,25.2,40,0,25
|
||||
2026-06-11T11:29:26,1899,25.2,40,0,25
|
||||
2026-06-11T11:30:26,1899,25.2,40,0,25
|
||||
2026-06-11T11:31:27,1889,25.2,40,0,25
|
||||
2026-06-11T11:32:27,1889,25.2,40,0,25
|
||||
2026-06-11T11:33:28,1889,25.1,40,0,25
|
||||
2026-06-11T11:34:28,1889,25.1,40,0,25
|
||||
2026-06-11T11:35:29,1871,25.1,39,0,25
|
||||
2026-06-11T11:36:29,1897,25.1,39,0,25
|
||||
2026-06-11T11:37:30,1903,25.1,39,0,25
|
||||
2026-06-11T11:38:30,1922,25.2,39,0,25
|
||||
2026-06-11T11:39:31,1930,25.2,40,0,25
|
||||
2026-06-11T11:40:31,1928,25.2,40,0,25
|
||||
2026-06-11T11:41:32,1946,25.2,39,0,25
|
||||
2026-06-11T11:43:33,1980,25.3,39,0,25
|
||||
2026-06-11T11:44:33,2001,25.3,39,0,25
|
||||
2026-06-11T11:45:34,1980,25.3,39,0,25
|
||||
2026-06-11T11:46:34,2001,25.3,40,0,25
|
||||
2026-06-11T11:47:35,2004,25.3,39,0,25
|
||||
2026-06-11T11:48:35,2028,25.5,40,0,25
|
||||
2026-06-11T11:49:36,2025,25.5,40,0,25
|
||||
2026-06-11T11:50:36,2025,25.3,40,0,25
|
||||
2026-06-11T11:51:37,2025,25.5,40,0,25
|
||||
2026-06-11T11:52:37,2035,25.5,40,0,25
|
||||
2026-06-11T11:53:38,2054,25.5,40,0,25
|
||||
2026-06-11T11:54:38,2054,25.5,40,0,25
|
||||
2026-06-11T11:55:39,2054,25.5,39,0,25
|
||||
2026-06-11T11:56:39,2070,25.5,39,0,25
|
||||
2026-06-11T11:57:40,2070,25.5,40,0,25
|
||||
2026-06-11T11:58:40,2035,25.5,39,0,25
|
||||
2026-06-11T11:59:41,2070,25.5,39,0,25
|
||||
2026-06-11T12:00:41,2080,25.3,40,0,25
|
||||
2026-06-11T12:01:42,2066,25.5,39,0,25
|
||||
2026-06-11T12:02:42,2060,25.5,40,0,25
|
||||
2026-06-11T12:03:43,2054,25.5,40,0,25
|
||||
2026-06-11T12:04:43,2054,25.5,40,0,25
|
||||
2026-06-11T12:05:44,2035,25.3,40,0,25
|
||||
2026-06-11T12:06:44,2054,25.5,39,0,25
|
||||
2026-06-11T12:07:45,2080,25.5,40,0,25
|
||||
2026-06-11T12:08:45,2054,25.5,40,0,25
|
||||
2026-06-11T12:09:46,2054,25.5,40,0,25
|
||||
2026-06-11T12:10:46,2066,25.5,40,0,25
|
||||
2026-06-11T12:11:47,2066,25.5,39,0,25
|
||||
2026-06-11T12:12:48,2080,25.5,39,0,25
|
||||
2026-06-11T12:13:48,2080,25.5,39,0,25
|
||||
2026-06-11T12:14:48,2088,25.5,39,0,25
|
||||
2026-06-11T12:15:49,2080,25.3,39,0,25
|
||||
2026-06-11T12:16:49,2080,25.3,39,0,25
|
||||
2026-06-11T12:17:50,2060,25.5,39,0,25
|
||||
2026-06-11T12:18:50,2048,25.5,39,0,25
|
||||
2026-06-11T12:19:51,2048,25.3,38,0,25
|
||||
2026-06-11T12:20:51,2074,25.5,38,0,25
|
||||
2026-06-11T12:21:52,2074,25.5,38,0,25
|
||||
2026-06-11T12:22:53,2066,25.5,39,0,25
|
||||
2026-06-11T12:23:53,2048,25.5,39,0,25
|
||||
2026-06-11T12:24:53,2048,25.5,39,0,25
|
||||
2026-06-11T12:25:54,2028,25.5,39,0,25
|
||||
2026-06-11T12:26:55,2028,25.3,38,0,25
|
||||
2026-06-11T12:27:55,2028,25.5,38,0,22
|
||||
2026-06-11T12:28:56,2052,25.5,39,0,25
|
||||
2026-06-11T12:29:56,2028,25.5,39,0,25
|
||||
2026-06-11T12:30:57,2028,25.5,38,0,25
|
||||
2026-06-11T12:31:57,2052,25.5,38,0,25
|
||||
2026-06-11T12:32:57,2052,25.5,38,0,25
|
||||
2026-06-11T12:33:58,2052,25.5,38,0,25
|
||||
2026-06-11T12:35:59,2052,25.5,38,0,25
|
||||
2026-06-11T12:37:00,2052,25.5,38,0,25
|
||||
2026-06-11T12:38:00,2066,25.5,39,0,25
|
||||
2026-06-11T12:39:00,2048,25.5,38,0,25
|
||||
2026-06-11T12:40:01,2074,25.5,38,0,25
|
||||
2026-06-11T12:41:01,2074,25.5,38,0,25
|
||||
2026-06-11T12:42:02,2084,25.5,38,0,25
|
||||
2026-06-11T12:43:02,2094,25.6,38,0,25
|
||||
2026-06-11T12:44:03,2094,25.6,38,0,25
|
||||
2026-06-11T12:45:04,2084,25.6,38,0,25
|
||||
2026-06-11T12:46:04,2094,25.6,38,0,25
|
||||
2026-06-11T12:47:04,2094,25.6,38,0,25
|
||||
2026-06-11T12:48:05,2094,25.6,38,0,25
|
||||
2026-06-11T12:49:05,2094,25.6,38,0,25
|
||||
2026-06-11T12:50:06,2094,25.6,38,0,25
|
||||
2026-06-11T12:51:07,2084,25.6,38,0,25
|
||||
2026-06-11T12:52:07,2084,25.6,39,0,25
|
||||
2026-06-11T12:53:07,2060,25.7,38,0,25
|
||||
2026-06-11T12:54:08,2094,25.6,39,0,25
|
||||
2026-06-11T12:55:08,2132,25.7,38,0,25
|
||||
2026-06-11T12:56:09,2094,25.6,38,0,25
|
||||
2026-06-11T12:57:10,2094,25.7,39,0,25
|
||||
2026-06-11T12:58:10,2098,25.7,38,0,25
|
||||
2026-06-11T12:59:10,2138,25.7,38,0,25
|
||||
2026-06-11T13:00:11,2116,25.7,39,0,22
|
||||
2026-06-11T13:01:12,2098,25.7,38,0,25
|
||||
2026-06-11T13:02:12,2116,25.7,38,0,25
|
||||
2026-06-11T13:03:12,2116,25.7,38,0,25
|
||||
2026-06-11T13:04:13,2116,25.7,38,0,25
|
||||
2026-06-11T13:05:13,2104,25.7,38,0,22
|
||||
2026-06-11T13:06:14,2116,25.7,38,0,25
|
||||
2026-06-11T13:07:15,2126,25.7,38,0,25
|
||||
2026-06-11T13:08:15,2126,25.8,38,0,25
|
||||
2026-06-11T13:09:16,2138,25.8,39,0,25
|
||||
2026-06-11T13:10:16,2120,25.8,39,0,25
|
||||
2026-06-11T13:11:16,2144,25.8,39,0,22
|
||||
2026-06-11T13:12:17,2110,25.8,39,0,25
|
||||
2026-06-11T13:13:18,2120,25.8,38,0,25
|
||||
2026-06-11T13:14:18,2150,25.8,38,0,25
|
||||
2026-06-11T13:15:18,2162,25.8,38,0,25
|
||||
2026-06-11T13:16:19,2162,25.8,38,0,25
|
||||
2026-06-11T13:17:19,2172,25.8,38,0,25
|
||||
2026-06-11T13:18:20,2184,26,38,0,25
|
||||
2026-06-11T13:19:21,2102,26,38,0,25
|
||||
2026-06-11T13:20:21,2102,26.1,39,0,25
|
||||
2026-06-11T13:21:21,2098,26,39,0,25
|
||||
2026-06-11T13:22:22,2098,26,39,0,25
|
||||
2026-06-11T13:23:22,2120,26.1,38,0,22
|
||||
2026-06-11T13:24:23,2136,26.1,39,0,25
|
||||
2026-06-11T13:25:24,2120,26.1,38,0,25
|
||||
2026-06-11T13:26:24,2136,26.1,38,0,25
|
||||
2026-06-11T13:27:25,2150,26.1,38,0,25
|
||||
2026-06-11T13:28:25,2162,26.1,38,0,25
|
||||
2026-06-11T13:29:26,2162,26.1,38,0,25
|
||||
2026-06-11T13:30:26,2150,26.1,38,0,25
|
||||
2026-06-11T13:31:27,2162,26.1,38,0,25
|
||||
2026-06-11T13:32:27,2172,26.2,38,0,25
|
||||
2026-06-11T13:33:28,2162,26.1,38,0,25
|
||||
2026-06-11T13:34:28,2136,26.2,38,0,25
|
||||
2026-06-11T13:35:29,2136,26.2,39,0,25
|
||||
2026-06-11T13:36:29,2120,26.1,38,0,25
|
||||
2026-06-11T13:37:30,2136,26.1,38,0,22
|
||||
2026-06-11T13:38:30,2136,26.1,38,0,22
|
||||
2026-06-11T13:39:31,2150,26.1,38,0,22
|
||||
2026-06-11T13:40:31,2150,26.2,38,0,22
|
||||
2026-06-11T13:41:32,2150,26.2,38,0,25
|
||||
2026-06-11T13:42:32,2150,26.2,38,0,25
|
||||
2026-06-11T13:43:33,2162,26.2,37,0,22
|
||||
2026-06-11T13:44:33,2166,26.2,38,0,25
|
||||
2026-06-11T13:45:34,2150,26.2,38,0,25
|
||||
2026-06-11T13:46:34,2136,26.2,38,0,25
|
||||
2026-06-11T13:47:35,2150,26.2,38,0,25
|
||||
2026-06-11T13:48:35,2136,26.2,38,0,25
|
||||
2026-06-11T13:49:36,2136,26.2,38,0,25
|
||||
2026-06-11T13:50:36,2124,26.2,38,0,25
|
||||
2026-06-11T13:51:37,2124,26.2,38,0,25
|
||||
2026-06-11T13:52:37,2124,26.2,39,0,22
|
||||
2026-06-11T13:53:38,2120,26.1,38,0,25
|
||||
2026-06-11T13:54:38,2136,26.2,38,0,25
|
||||
2026-06-11T13:55:39,2150,26.1,38,0,25
|
||||
2026-06-11T13:56:39,2150,26.2,38,0,25
|
||||
2026-06-11T13:57:40,2124,26.2,38,0,25
|
||||
2026-06-11T13:58:40,2124,26.2,38,0,25
|
||||
2026-06-11T13:59:41,2124,26.2,38,0,25
|
||||
2026-06-11T14:00:41,2124,26.2,37,0,25
|
||||
2026-06-11T14:01:42,2154,26.2,38,0,25
|
||||
2026-06-11T14:02:42,2136,26.2,37,0,25
|
||||
2026-06-11T14:03:43,2142,26.2,38,0,25
|
||||
2026-06-11T14:04:43,2136,26.2,38,0,25
|
||||
2026-06-11T14:05:44,2136,26.2,37,0,25
|
||||
2026-06-11T14:06:44,2142,26.2,37,0,25
|
||||
2026-06-11T14:08:45,2142,26.2,37,0,25
|
||||
2026-06-11T14:09:46,2154,26.2,37,0,25
|
||||
2026-06-11T14:11:47,2166,26.2,38,0,25
|
||||
2026-06-11T14:12:48,2136,26.2,37,0,25
|
||||
2026-06-11T14:13:48,2154,26.2,38,0,25
|
||||
2026-06-11T14:14:48,2136,26.2,38,0,22
|
||||
2026-06-11T14:16:49,2124,26.2,38,0,22
|
||||
2026-06-11T14:17:50,2124,26.2,38,0,25
|
||||
2026-06-11T14:18:50,2124,26.2,38,0,25
|
||||
2026-06-11T14:19:51,2114,26.2,38,0,25
|
||||
2026-06-11T14:20:51,2102,26.2,37,0,25
|
||||
2026-06-11T14:21:52,2118,26.2,37,0,25
|
||||
2026-06-11T14:22:52,2130,26.2,37,0,25
|
||||
2026-06-11T14:23:53,2154,26.2,37,0,25
|
||||
2026-06-11T14:24:53,2154,26.2,37,0,25
|
||||
2026-06-11T14:25:54,2142,26.2,37,0,22
|
||||
2026-06-11T14:26:54,2130,26.3,37,0,25
|
||||
2026-06-11T14:27:55,2142,26.2,37,0,25
|
||||
2026-06-11T14:28:55,2166,26.2,37,0,22
|
||||
2026-06-11T14:29:56,2180,26.3,37,0,25
|
||||
2026-06-11T14:30:56,2189,26.3,37,0,22
|
||||
2026-06-11T14:31:57,2203,26.3,37,0,22
|
||||
2026-06-11T14:32:57,2203,26.2,37,0,25
|
||||
2026-06-11T14:33:58,2203,26.3,37,0,22
|
||||
2026-06-11T14:34:58,2214,26.3,37,0,25
|
||||
2026-06-11T14:35:59,2203,26.3,37,0,25
|
||||
2026-06-11T14:36:59,2214,26.3,37,0,25
|
||||
2026-06-11T14:38:00,2214,26.3,37,0,25
|
||||
2026-06-11T14:39:00,2214,26.3,37,0,25
|
||||
2026-06-11T14:40:01,2214,26.3,37,0,22
|
||||
2026-06-11T14:41:01,2203,26.3,37,0,22
|
||||
2026-06-11T14:42:02,2203,26.3,37,0,22
|
||||
2026-06-11T14:43:02,2203,26.3,37,0,22
|
||||
2026-06-11T14:44:03,2214,26.3,37,0,25
|
||||
2026-06-11T14:45:03,2189,26.3,37,0,25
|
||||
2026-06-11T14:46:04,2189,26.3,37,0,22
|
||||
2026-06-11T14:47:04,2189,26.3,37,0,25
|
||||
2026-06-11T14:48:05,2189,26.3,37,0,22
|
||||
2026-06-11T14:49:05,2189,26.3,37,0,22
|
||||
2026-06-11T14:50:06,2189,26.3,37,0,25
|
||||
2026-06-11T14:51:06,2189,26.3,37,0,25
|
||||
2026-06-11T14:52:07,2180,26.3,37,0,22
|
||||
2026-06-11T14:53:07,2203,26.3,37,0,22
|
||||
2026-06-11T14:54:08,2203,26.3,38,0,22
|
||||
2026-06-11T14:55:08,2197,26.3,37,0,22
|
||||
2026-06-11T14:56:09,2214,26.3,37,0,25
|
||||
2026-06-11T14:57:09,2214,26.5,37,0,22
|
||||
2026-06-11T14:58:10,2240,26.5,37,0,22
|
||||
2026-06-11T14:59:10,2255,26.5,38,0,25
|
||||
2026-06-11T15:00:11,2236,26.5,37,0,22
|
||||
2026-06-11T15:01:11,2255,26.5,38,0,25
|
||||
2026-06-11T15:02:12,2220,26.6,37,0,25
|
||||
2026-06-11T15:03:12,2228,26.6,38,0,22
|
||||
2026-06-11T15:04:13,2236,26.6,38,0,25
|
||||
2026-06-11T15:05:13,2249,26.6,37,0,25
|
||||
2026-06-11T15:06:14,2265,26.6,37,0,22
|
||||
2026-06-11T15:07:14,2279,26.6,37,0,25
|
||||
2026-06-11T15:08:15,2265,26.7,37,0,22
|
||||
2026-06-11T15:09:15,2292,26.7,37,0,22
|
||||
2026-06-11T15:10:16,2279,26.7,37,0,22
|
||||
2026-06-11T15:11:17,2265,26.7,37,0,22
|
||||
2026-06-11T15:12:17,2265,26.7,37,0,25
|
||||
2026-06-11T15:13:17,2265,26.7,37,0,22
|
||||
2026-06-11T15:14:18,2279,26.7,37,0,22
|
||||
2026-06-11T15:15:18,2265,26.7,37,0,22
|
||||
2026-06-11T15:16:19,2279,26.7,37,0,22
|
||||
2026-06-11T15:17:20,2292,26.7,37,0,25
|
||||
2026-06-11T15:18:20,2265,26.7,37,0,25
|
||||
2026-06-11T15:20:21,2255,26.7,37,0,22
|
||||
2026-06-11T15:21:22,2265,26.7,37,0,25
|
||||
2026-06-11T15:22:22,2255,26.7,37,0,22
|
||||
2026-06-11T15:23:22,2265,26.7,37,0,22
|
||||
2026-06-11T15:24:23,2255,26.7,37,0,22
|
||||
2026-06-11T15:25:23,2279,26.7,37,0,22
|
||||
2026-06-11T15:26:24,2279,26.7,37,0,22
|
||||
2026-06-11T15:27:24,2279,26.7,37,0,25
|
||||
2026-06-11T15:28:25,2292,26.7,37,0,22
|
||||
2026-06-11T15:29:26,2279,26.7,37,0,22
|
||||
2026-06-11T15:30:26,2292,26.8,37,0,25
|
||||
2026-06-11T15:31:27,2334,26.7,37,0,22
|
||||
2026-06-11T15:32:27,2292,26.8,36,0,22
|
||||
2026-06-11T15:33:28,2324,26.7,37,0,22
|
||||
2026-06-11T15:34:28,2292,26.8,37,0,22
|
||||
2026-06-11T15:35:28,2318,26.8,36,0,25
|
||||
2026-06-11T15:36:29,2265,26.8,37,0,22
|
||||
2026-06-11T15:37:30,2242,26.8,37,0,25
|
||||
2026-06-11T15:38:30,2270,26.8,37,0,22
|
||||
2026-06-11T15:39:30,2255,26.8,37,0,22
|
||||
2026-06-11T15:40:31,2259,26.8,37,0,22
|
||||
2026-06-11T15:41:32,2281,26.8,37,0,25
|
||||
2026-06-11T15:42:32,2285,26.8,37,0,25
|
||||
2026-06-11T15:44:33,2281,26.8,37,0,22
|
||||
2026-06-11T15:45:33,2285,26.8,37,0,25
|
||||
2026-06-11T15:47:34,2249,26.8,36,0,22
|
||||
2026-06-11T15:48:35,2226,26.8,37,0,22
|
||||
2026-06-11T15:49:36,2226,26.8,37,0,22
|
||||
2026-06-11T15:50:36,2259,26.8,37,0,22
|
||||
2026-06-11T15:51:37,2255,26.8,37,0,22
|
||||
2026-06-11T15:52:37,2249,26.8,37,0,22
|
||||
2026-06-11T15:53:37,2263,26.8,37,0,22
|
||||
2026-06-11T15:54:38,2259,26.8,36,0,22
|
||||
2026-06-11T15:55:38,2278,26.8,36,0,22
|
||||
2026-06-11T15:56:39,2287,27,37,0,22
|
||||
2026-06-11T15:57:39,2222,26.8,36,0,22
|
||||
2026-06-11T15:58:40,2281,26.8,37,0,22
|
||||
2026-06-11T15:59:40,2276,27,36,0,22
|
||||
2026-06-11T16:00:41,2222,27,37,0,22
|
||||
2026-06-11T16:01:41,2189,27,36,0,22
|
||||
2026-06-11T16:03:43,2205,27,37,0,22
|
||||
2026-06-11T16:04:43,2195,27,36,0,22
|
||||
2026-06-11T16:05:43,2218,27,36,0,25
|
||||
2026-06-11T16:06:44,2207,26.8,37,0,22
|
||||
2026-06-11T16:07:44,2259,27,37,0,22
|
||||
2026-06-11T16:08:45,2189,26.8,37,0,22
|
||||
2026-06-11T16:09:46,2259,26.8,37,0,22
|
||||
2026-06-11T16:10:46,2276,26.8,37,0,22
|
||||
2026-06-11T16:11:47,2281,27,37,0,22
|
||||
2026-06-11T16:13:48,2210,27,37,0,22
|
||||
2026-06-11T16:14:48,2205,27,37,0,22
|
||||
2026-06-11T16:15:49,2210,27.1,37,0,22
|
||||
2026-06-11T16:16:49,2210,26.8,37,0,22
|
||||
2026-06-11T16:17:50,2296,27.3,37,0,22
|
||||
2026-06-11T16:18:50,2210,27,37,0,22
|
||||
2026-06-11T16:19:50,2245,27,37,0,22
|
||||
2026-06-11T16:20:51,2232,27.1,37,0,22
|
||||
2026-06-11T16:21:51,2212,27,37,0,22
|
||||
2026-06-11T16:22:52,2222,27,37,0,22
|
||||
2026-06-11T16:23:53,2251,27,37,0,22
|
||||
2026-06-11T16:25:53,2222,27.1,37,0,22
|
||||
2026-06-11T16:26:54,2222,27.1,37,1,22
|
||||
2026-06-11T16:28:55,2224,27.1,37,1,22
|
||||
2026-06-11T16:29:56,2224,27.1,37,1,22
|
||||
|
444
report/resources/measures/A3-DC_06_D9_40_7A_CB.csv
Normal file
@@ -0,0 +1,444 @@
|
||||
time,co2,temperature,humidity,windows,battery
|
||||
2026-06-11T08:45:43,496,27.7,38,0,57
|
||||
2026-06-11T08:46:43,496,27.7,38,0,55
|
||||
2026-06-11T08:47:44,491,27.8,37,0,57
|
||||
2026-06-11T08:49:45,527,28,37,0,57
|
||||
2026-06-11T08:50:45,459,28,37,0,57
|
||||
2026-06-11T08:51:46,448,28.1,37,0,57
|
||||
2026-06-11T08:52:46,486,28.2,37,0,57
|
||||
2026-06-11T08:53:47,463,28.2,37,0,57
|
||||
2026-06-11T08:54:47,445,28.2,37,0,57
|
||||
2026-06-11T08:55:48,454,28.3,37,0,57
|
||||
2026-06-11T08:56:48,486,28.3,37,0,57
|
||||
2026-06-11T08:57:49,459,28.3,37,0,57
|
||||
2026-06-11T08:58:49,466,28.5,37,0,57
|
||||
2026-06-11T08:59:50,476,28.5,37,0,57
|
||||
2026-06-11T09:00:50,491,28.6,37,0,57
|
||||
2026-06-11T09:01:51,481,28.6,37,0,55
|
||||
2026-06-11T09:02:51,481,28.6,37,0,57
|
||||
2026-06-11T09:03:52,486,28.7,37,0,57
|
||||
2026-06-11T09:04:52,481,28.7,36,0,57
|
||||
2026-06-11T09:05:53,518,28.8,36,0,57
|
||||
2026-06-11T09:06:53,502,28.7,37,0,57
|
||||
2026-06-11T09:07:54,502,28.8,36,0,57
|
||||
2026-06-11T09:08:54,536,29,37,0,57
|
||||
2026-06-11T09:09:55,450,29,36,0,57
|
||||
2026-06-11T09:10:55,472,29,36,0,57
|
||||
2026-06-11T09:11:56,477,29,36,0,57
|
||||
2026-06-11T09:12:56,445,29.1,36,0,55
|
||||
2026-06-11T09:13:57,464,29.1,36,0,57
|
||||
2026-06-11T09:15:58,455,29.2,36,0,57
|
||||
2026-06-11T09:16:58,472,29.3,36,0,57
|
||||
2026-06-11T09:17:59,498,29.3,35,0,57
|
||||
2026-06-11T09:18:59,493,29.5,36,0,57
|
||||
2026-06-11T09:20:00,464,29.5,35,0,57
|
||||
2026-06-11T09:21:00,478,29.5,35,0,57
|
||||
2026-06-11T09:22:01,493,29.6,35,0,57
|
||||
2026-06-11T09:23:01,478,29.6,35,0,57
|
||||
2026-06-11T09:24:02,519,29.6,35,0,57
|
||||
2026-06-11T09:25:02,478,29.5,35,0,57
|
||||
2026-06-11T09:26:03,472,29.6,35,0,57
|
||||
2026-06-11T09:27:03,514,29.6,35,0,57
|
||||
2026-06-11T09:28:04,514,29.5,35,0,57
|
||||
2026-06-11T09:29:05,500,29.7,35,0,57
|
||||
2026-06-11T09:30:05,493,29.7,35,0,57
|
||||
2026-06-11T09:31:05,519,29.7,34,0,57
|
||||
2026-06-11T09:32:06,503,29.7,35,0,57
|
||||
2026-06-11T09:33:07,487,29.6,35,0,57
|
||||
2026-06-11T09:34:07,500,29.7,34,0,57
|
||||
2026-06-11T09:35:07,580,29.7,34,0,57
|
||||
2026-06-11T09:36:08,520,29.7,34,0,57
|
||||
2026-06-11T09:37:08,514,29.7,34,0,57
|
||||
2026-06-11T09:38:09,520,29.7,35,0,57
|
||||
2026-06-11T09:39:09,500,29.7,34,0,57
|
||||
2026-06-11T09:40:10,498,29.7,35,0,57
|
||||
2026-06-11T09:41:10,493,29.7,34,0,57
|
||||
2026-06-11T09:42:11,520,29.7,35,0,57
|
||||
2026-06-11T09:43:11,527,29.7,34,0,57
|
||||
2026-06-11T09:44:12,537,29.7,34,0,57
|
||||
2026-06-11T09:45:12,537,29.7,34,0,55
|
||||
2026-06-11T09:46:13,526,29.7,34,0,57
|
||||
2026-06-11T09:47:13,531,29.7,34,0,57
|
||||
2026-06-11T09:48:14,503,29.7,34,0,57
|
||||
2026-06-11T09:49:15,514,29.7,34,0,57
|
||||
2026-06-11T09:50:15,520,29.7,34,0,57
|
||||
2026-06-11T09:51:15,514,29.7,35,0,57
|
||||
2026-06-11T09:52:16,500,29.8,34,0,57
|
||||
2026-06-11T09:53:17,514,29.7,35,0,57
|
||||
2026-06-11T09:54:17,514,29.8,34,0,57
|
||||
2026-06-11T09:55:17,531,29.8,34,0,57
|
||||
2026-06-11T09:56:18,520,29.8,34,0,57
|
||||
2026-06-11T09:57:18,537,29.8,34,0,57
|
||||
2026-06-11T09:58:19,520,29.8,34,0,57
|
||||
2026-06-11T09:59:19,537,29.7,34,0,57
|
||||
2026-06-11T10:00:20,545,29.8,34,0,55
|
||||
2026-06-11T10:01:20,551,29.8,34,0,57
|
||||
2026-06-11T10:02:21,574,29.8,34,0,57
|
||||
2026-06-11T10:03:22,520,29.8,34,0,57
|
||||
2026-06-11T10:04:22,537,29.8,34,0,57
|
||||
2026-06-11T10:05:22,537,29.8,34,0,57
|
||||
2026-06-11T10:06:23,545,29.8,34,0,57
|
||||
2026-06-11T10:07:23,551,29.8,34,0,57
|
||||
2026-06-11T10:08:24,545,29.8,34,0,57
|
||||
2026-06-11T10:09:25,537,29.8,34,0,57
|
||||
2026-06-11T10:10:25,556,29.8,34,0,55
|
||||
2026-06-11T10:11:25,531,29.8,35,0,57
|
||||
2026-06-11T10:12:26,514,29.8,34,0,57
|
||||
2026-06-11T10:13:26,537,29.8,34,0,57
|
||||
2026-06-11T10:15:28,551,29.8,34,0,57
|
||||
2026-06-11T10:16:28,526,29.8,34,0,57
|
||||
2026-06-11T10:17:29,537,29.8,34,0,57
|
||||
2026-06-11T10:18:29,537,29.8,34,0,57
|
||||
2026-06-11T10:19:29,498,29.8,34,0,57
|
||||
2026-06-11T10:20:30,537,29.8,35,0,57
|
||||
2026-06-11T10:21:30,527,29.7,34,0,57
|
||||
2026-06-11T10:22:31,514,29.7,34,0,57
|
||||
2026-06-11T10:23:32,545,29.6,34,0,57
|
||||
2026-06-11T10:24:32,537,29.6,35,0,57
|
||||
2026-06-11T10:25:32,533,29.5,35,0,57
|
||||
2026-06-11T10:26:33,539,29.5,35,0,57
|
||||
2026-06-11T10:27:33,482,29.5,35,0,57
|
||||
2026-06-11T10:28:34,500,29.5,35,0,57
|
||||
2026-06-11T10:29:35,539,29.5,35,0,57
|
||||
2026-06-11T10:30:35,487,29.5,35,0,57
|
||||
2026-06-11T10:31:36,500,29.5,34,0,57
|
||||
2026-06-11T10:32:36,514,29.5,35,0,57
|
||||
2026-06-11T10:33:37,527,29.5,35,0,55
|
||||
2026-06-11T10:34:37,500,29.5,35,0,57
|
||||
2026-06-11T10:35:38,503,29.5,34,0,57
|
||||
2026-06-11T10:36:38,537,29.5,35,0,57
|
||||
2026-06-11T10:37:39,539,29.6,35,0,57
|
||||
2026-06-11T10:38:39,493,29.6,35,0,57
|
||||
2026-06-11T10:39:40,561,29.6,35,0,57
|
||||
2026-06-11T10:40:40,500,29.7,35,0,57
|
||||
2026-06-11T10:41:41,533,29.6,35,0,57
|
||||
2026-06-11T10:42:41,533,29.7,35,0,57
|
||||
2026-06-11T10:43:42,493,29.7,34,0,57
|
||||
2026-06-11T10:44:42,526,29.7,35,0,57
|
||||
2026-06-11T10:45:43,533,29.7,35,0,55
|
||||
2026-06-11T10:46:43,545,29.8,35,0,55
|
||||
2026-06-11T10:47:44,545,29.7,34,0,57
|
||||
2026-06-11T10:48:44,531,29.7,34,0,57
|
||||
2026-06-11T10:49:45,520,29.7,35,0,57
|
||||
2026-06-11T10:50:45,520,29.8,34,0,55
|
||||
2026-06-11T10:51:46,611,29.8,35,0,57
|
||||
2026-06-11T10:52:46,503,29.8,35,0,55
|
||||
2026-06-11T10:53:47,478,29.8,34,0,55
|
||||
2026-06-11T10:54:47,537,29.8,34,0,55
|
||||
2026-06-11T10:55:48,514,29.7,35,0,57
|
||||
2026-06-11T10:56:48,519,29.8,35,0,55
|
||||
2026-06-11T10:57:49,539,29.8,34,0,55
|
||||
2026-06-11T10:58:49,588,30,34,0,55
|
||||
2026-06-11T10:59:50,526,29.8,35,0,55
|
||||
2026-06-11T11:00:50,514,29.8,34,0,55
|
||||
2026-06-11T11:01:51,545,30,34,0,57
|
||||
2026-06-11T11:02:51,523,29.8,35,0,57
|
||||
2026-06-11T11:03:52,533,29.8,35,0,55
|
||||
2026-06-11T11:04:52,561,29.8,34,0,57
|
||||
2026-06-11T11:05:53,545,29.8,34,0,57
|
||||
2026-06-11T11:06:53,574,29.8,35,0,57
|
||||
2026-06-11T11:07:54,519,30,34,0,57
|
||||
2026-06-11T11:08:54,481,30,35,0,57
|
||||
2026-06-11T11:09:55,491,29.8,35,0,57
|
||||
2026-06-11T11:11:56,514,30,34,0,57
|
||||
2026-06-11T11:12:56,476,29.8,34,0,57
|
||||
2026-06-11T11:13:57,556,30,34,0,57
|
||||
2026-06-11T11:14:58,534,30,35,0,57
|
||||
2026-06-11T11:15:58,463,30,34,0,57
|
||||
2026-06-11T11:16:58,506,30,35,0,57
|
||||
2026-06-11T11:17:59,496,30,35,0,57
|
||||
2026-06-11T11:18:59,496,30,34,0,57
|
||||
2026-06-11T11:20:00,463,30,34,0,57
|
||||
2026-06-11T11:21:00,488,30,34,0,57
|
||||
2026-06-11T11:22:01,488,30,34,0,57
|
||||
2026-06-11T11:23:02,470,30,34,0,57
|
||||
2026-06-11T11:24:02,488,30,34,0,57
|
||||
2026-06-11T11:25:02,500,30,34,0,57
|
||||
2026-06-11T11:26:03,495,30,34,0,57
|
||||
2026-06-11T11:27:03,481,30,34,0,57
|
||||
2026-06-11T11:28:04,457,30,34,0,57
|
||||
2026-06-11T11:29:04,500,30.1,34,0,57
|
||||
2026-06-11T11:30:05,529,30,34,0,57
|
||||
2026-06-11T11:31:05,476,30,34,0,57
|
||||
2026-06-11T11:32:06,506,30,34,0,57
|
||||
2026-06-11T11:33:06,452,30.1,34,0,57
|
||||
2026-06-11T11:34:07,495,30,34,0,57
|
||||
2026-06-11T11:36:08,463,30,35,0,57
|
||||
2026-06-11T11:37:09,438,30,34,0,57
|
||||
2026-06-11T11:38:09,517,30.1,34,0,57
|
||||
2026-06-11T11:40:10,481,30,34,0,57
|
||||
2026-06-11T11:41:10,470,30.1,34,0,57
|
||||
2026-06-11T11:42:11,463,30.1,34,0,57
|
||||
2026-06-11T11:43:11,470,30,35,0,57
|
||||
2026-06-11T11:44:12,473,30,34,0,57
|
||||
2026-06-11T11:45:12,488,30,34,0,57
|
||||
2026-06-11T11:46:13,470,30,34,0,57
|
||||
2026-06-11T11:47:13,523,30,34,0,57
|
||||
2026-06-11T11:48:14,488,30,34,0,57
|
||||
2026-06-11T11:49:14,452,30,34,0,57
|
||||
2026-06-11T11:50:15,476,30,34,0,57
|
||||
2026-06-11T11:51:15,488,30,35,0,57
|
||||
2026-06-11T11:52:16,501,30.1,35,0,57
|
||||
2026-06-11T11:53:16,454,30.1,34,0,57
|
||||
2026-06-11T11:55:17,470,30,34,0,55
|
||||
2026-06-11T11:56:18,470,30,35,0,57
|
||||
2026-06-11T11:57:19,459,30,35,0,57
|
||||
2026-06-11T11:58:19,463,30,34,0,57
|
||||
2026-06-11T11:59:19,529,30,35,0,57
|
||||
2026-06-11T12:00:20,478,30.1,35,0,57
|
||||
2026-06-11T12:01:20,473,30,35,0,57
|
||||
2026-06-11T12:02:21,463,30,34,0,57
|
||||
2026-06-11T12:03:21,506,30.1,35,0,57
|
||||
2026-06-11T12:04:22,473,30.1,35,0,57
|
||||
2026-06-11T12:05:23,496,30.1,34,0,57
|
||||
2026-06-11T12:06:23,546,30.1,34,0,57
|
||||
2026-06-11T12:07:23,534,30.1,34,0,57
|
||||
2026-06-11T12:08:24,517,30.1,34,0,57
|
||||
2026-06-11T12:09:24,500,30.1,35,0,57
|
||||
2026-06-11T12:10:25,506,30.1,34,0,57
|
||||
2026-06-11T12:11:25,500,30.1,34,0,57
|
||||
2026-06-11T12:12:26,561,30.1,34,0,57
|
||||
2026-06-11T12:13:27,529,30.1,35,0,57
|
||||
2026-06-11T12:14:27,478,30.1,34,0,57
|
||||
2026-06-11T12:15:28,517,30.1,34,0,57
|
||||
2026-06-11T12:16:28,523,30,34,0,57
|
||||
2026-06-11T12:17:28,517,30.1,35,0,57
|
||||
2026-06-11T12:18:29,491,30.1,35,0,57
|
||||
2026-06-11T12:19:29,506,30.2,34,0,57
|
||||
2026-06-11T12:20:30,506,30.1,34,0,57
|
||||
2026-06-11T12:21:30,567,30.1,34,0,57
|
||||
2026-06-11T12:22:31,517,30.1,34,0,57
|
||||
2026-06-11T12:23:31,523,30.1,34,0,57
|
||||
2026-06-11T12:24:32,506,30.1,34,0,57
|
||||
2026-06-11T12:25:32,523,30.1,34,0,57
|
||||
2026-06-11T12:26:33,517,30.1,34,0,57
|
||||
2026-06-11T12:27:33,523,30.1,34,0,57
|
||||
2026-06-11T12:28:34,567,30.2,34,0,57
|
||||
2026-06-11T12:29:35,517,30.2,35,0,57
|
||||
2026-06-11T12:30:35,506,30.2,34,0,57
|
||||
2026-06-11T12:31:36,529,30.1,34,0,57
|
||||
2026-06-11T12:32:36,534,30.2,35,0,57
|
||||
2026-06-11T12:33:36,501,30.2,34,0,57
|
||||
2026-06-11T12:34:37,529,30.2,34,0,57
|
||||
2026-06-11T12:35:38,599,30.2,34,0,57
|
||||
2026-06-11T12:36:38,567,30.2,34,0,57
|
||||
2026-06-11T12:37:39,534,30.2,34,0,57
|
||||
2026-06-11T12:38:39,552,30.1,34,0,57
|
||||
2026-06-11T12:39:40,534,30.2,34,0,60
|
||||
2026-06-11T12:40:40,546,30.2,34,0,57
|
||||
2026-06-11T12:41:41,592,30.2,34,0,57
|
||||
2026-06-11T12:42:41,534,30.2,34,0,57
|
||||
2026-06-11T12:43:42,552,30.2,34,0,57
|
||||
2026-06-11T12:44:42,546,30.2,34,0,57
|
||||
2026-06-11T12:45:43,546,30.3,34,0,57
|
||||
2026-06-11T12:46:43,592,30.2,34,0,57
|
||||
2026-06-11T12:47:44,529,30.2,34,0,57
|
||||
2026-06-11T12:48:44,534,30.2,34,0,57
|
||||
2026-06-11T12:49:45,540,30.3,34,0,57
|
||||
2026-06-11T12:50:45,561,30.3,34,0,57
|
||||
2026-06-11T12:51:46,552,30.3,35,0,57
|
||||
2026-06-11T12:52:46,517,30.2,34,0,57
|
||||
2026-06-11T12:53:47,546,30.2,34,0,57
|
||||
2026-06-11T12:54:47,567,30.3,34,0,57
|
||||
2026-06-11T12:55:48,534,30.2,34,0,57
|
||||
2026-06-11T12:56:48,552,30.2,34,0,57
|
||||
2026-06-11T12:57:49,552,30.2,34,0,57
|
||||
2026-06-11T12:58:49,546,30.3,34,0,57
|
||||
2026-06-11T12:59:50,534,30.3,34,0,57
|
||||
2026-06-11T13:00:50,580,30.3,34,0,57
|
||||
2026-06-11T13:01:51,534,30.3,34,0,57
|
||||
2026-06-11T13:02:51,561,30.3,34,0,57
|
||||
2026-06-11T13:03:52,561,30.3,34,0,57
|
||||
2026-06-11T13:04:52,592,30.3,34,0,57
|
||||
2026-06-11T13:06:53,537,30.3,34,0,57
|
||||
2026-06-11T13:07:54,574,30.3,34,0,57
|
||||
2026-06-11T13:09:55,517,30.3,34,0,57
|
||||
2026-06-11T13:10:55,529,30.3,34,0,57
|
||||
2026-06-11T13:11:56,546,30.3,34,0,57
|
||||
2026-06-11T13:12:56,561,30.3,34,0,57
|
||||
2026-06-11T13:13:57,540,30.3,33,0,57
|
||||
2026-06-11T13:14:57,569,30.2,34,0,57
|
||||
2026-06-11T13:15:58,580,30.2,34,0,57
|
||||
2026-06-11T13:16:58,552,30.2,34,0,57
|
||||
2026-06-11T13:17:59,534,30.1,34,0,55
|
||||
2026-06-11T13:18:59,523,30.1,35,0,57
|
||||
2026-06-11T13:20:00,542,30,34,0,57
|
||||
2026-06-11T13:21:00,523,29.8,35,0,57
|
||||
2026-06-11T13:22:01,567,29.8,34,0,57
|
||||
2026-06-11T13:23:01,611,29.8,35,0,55
|
||||
2026-06-11T13:24:02,589,29.7,35,0,57
|
||||
2026-06-11T13:25:03,553,29.8,34,0,57
|
||||
2026-06-11T13:26:03,565,29.8,34,0,57
|
||||
2026-06-11T13:27:03,580,29.7,34,0,57
|
||||
2026-06-11T13:28:04,551,29.7,34,0,57
|
||||
2026-06-11T13:29:04,580,29.7,35,0,57
|
||||
2026-06-11T13:30:05,539,29.7,35,0,57
|
||||
2026-06-11T13:31:05,539,29.7,35,0,57
|
||||
2026-06-11T13:32:06,567,29.7,35,0,57
|
||||
2026-06-11T13:34:07,556,29.8,35,0,57
|
||||
2026-06-11T13:35:07,545,29.8,34,0,57
|
||||
2026-06-11T13:36:08,551,30,34,0,57
|
||||
2026-06-11T13:37:09,500,30,35,0,57
|
||||
2026-06-11T13:38:09,496,30,35,0,55
|
||||
2026-06-11T13:39:09,506,30,35,0,57
|
||||
2026-06-11T13:40:10,478,30,34,0,57
|
||||
2026-06-11T13:41:10,546,30,35,0,57
|
||||
2026-06-11T13:42:11,501,30,34,0,57
|
||||
2026-06-11T13:44:12,461,29.8,35,0,55
|
||||
2026-06-11T13:45:12,539,29.3,34,0,57
|
||||
2026-06-11T13:46:13,556,29.1,35,0,57
|
||||
2026-06-11T13:47:14,567,28.7,35,0,55
|
||||
2026-06-11T13:48:14,607,28.6,36,0,57
|
||||
2026-06-11T13:49:14,588,28.3,35,0,57
|
||||
2026-06-11T13:50:15,580,28.2,36,0,57
|
||||
2026-06-11T13:51:16,561,28,36,0,57
|
||||
2026-06-11T13:52:16,553,27.8,36,0,55
|
||||
2026-06-11T13:53:16,653,27.7,36,0,57
|
||||
2026-06-11T13:54:17,618,27.7,36,0,57
|
||||
2026-06-11T13:55:17,612,27.5,37,0,57
|
||||
2026-06-11T13:56:18,575,27.5,37,0,57
|
||||
2026-06-11T13:57:18,556,27.3,37,0,55
|
||||
2026-06-11T13:58:19,570,27.2,37,0,55
|
||||
2026-06-11T13:59:19,591,27.2,37,0,55
|
||||
2026-06-11T14:00:20,533,27.1,37,0,55
|
||||
2026-06-11T14:01:20,549,27.1,38,0,57
|
||||
2026-06-11T14:02:21,501,27,37,0,55
|
||||
2026-06-11T14:03:22,523,27,37,0,55
|
||||
2026-06-11T14:04:22,523,26.8,38,0,57
|
||||
2026-06-11T14:05:22,537,26.7,38,0,55
|
||||
2026-06-11T14:06:23,589,26.7,38,0,57
|
||||
2026-06-11T14:07:23,542,26.7,38,0,57
|
||||
2026-06-11T14:08:24,567,26.6,38,0,57
|
||||
2026-06-11T14:09:24,564,26.6,38,0,57
|
||||
2026-06-11T14:10:25,567,26.6,38,0,57
|
||||
2026-06-11T14:11:26,575,26.6,38,0,57
|
||||
2026-06-11T14:12:26,575,26.6,37,0,57
|
||||
2026-06-11T14:13:27,575,26.5,38,0,55
|
||||
2026-06-11T14:15:28,508,26.5,38,0,55
|
||||
2026-06-11T14:16:28,549,26.3,38,0,55
|
||||
2026-06-11T14:17:28,529,26.3,38,0,55
|
||||
2026-06-11T14:18:29,529,26.5,39,0,57
|
||||
2026-06-11T14:19:30,476,26.3,38,0,57
|
||||
2026-06-11T14:20:30,514,26.3,39,0,55
|
||||
2026-06-11T14:21:31,492,26.2,38,0,55
|
||||
2026-06-11T14:22:31,501,26.2,38,0,55
|
||||
2026-06-11T14:23:32,514,26.2,39,0,57
|
||||
2026-06-11T14:24:32,495,26.2,38,0,57
|
||||
2026-06-11T14:25:32,581,26.2,39,0,55
|
||||
2026-06-11T14:26:33,624,26.2,38,0,55
|
||||
2026-06-11T14:27:33,682,26.2,39,0,55
|
||||
2026-06-11T14:28:34,675,26.2,39,0,55
|
||||
2026-06-11T14:29:35,671,26.2,39,0,55
|
||||
2026-06-11T14:30:35,629,26.1,39,0,55
|
||||
2026-06-11T14:31:36,638,26.2,39,0,57
|
||||
2026-06-11T14:32:36,638,26.1,38,0,55
|
||||
2026-06-11T14:33:37,615,26.2,38,0,55
|
||||
2026-06-11T14:34:37,607,26.2,39,0,55
|
||||
2026-06-11T14:35:37,638,26.1,39,0,55
|
||||
2026-06-11T14:36:38,600,26.1,39,0,55
|
||||
2026-06-11T14:37:39,575,26.2,39,0,55
|
||||
2026-06-11T14:38:39,575,26.1,39,0,55
|
||||
2026-06-11T14:39:40,596,26.1,39,0,57
|
||||
2026-06-11T14:40:40,624,26.1,39,0,55
|
||||
2026-06-11T14:41:41,575,26.1,39,0,57
|
||||
2026-06-11T14:42:41,565,26.1,38,0,55
|
||||
2026-06-11T14:43:42,637,26.1,39,0,55
|
||||
2026-06-11T14:44:42,581,26.1,39,0,57
|
||||
2026-06-11T14:45:43,581,26.1,39,0,55
|
||||
2026-06-11T14:46:43,556,26.1,39,0,55
|
||||
2026-06-11T14:49:45,565,26.1,39,0,57
|
||||
2026-06-11T14:50:45,565,26.1,39,0,55
|
||||
2026-06-11T14:51:46,545,26,39,0,55
|
||||
2026-06-11T14:52:46,526,26.1,39,0,55
|
||||
2026-06-11T14:53:47,549,26.1,39,0,55
|
||||
2026-06-11T14:54:47,549,26,38,0,55
|
||||
2026-06-11T14:55:48,575,26.1,39,0,55
|
||||
2026-06-11T14:56:48,533,26.1,39,0,55
|
||||
2026-06-11T14:58:49,526,26.1,39,0,55
|
||||
2026-06-11T14:59:50,526,26.1,39,0,55
|
||||
2026-06-11T15:00:50,565,26.1,39,0,57
|
||||
2026-06-11T15:01:51,520,26,39,0,55
|
||||
2026-06-11T15:02:51,526,26.1,38,0,57
|
||||
2026-06-11T15:03:52,526,26.1,38,0,55
|
||||
2026-06-11T15:04:52,549,26.1,39,0,55
|
||||
2026-06-11T15:05:53,539,26.1,39,0,57
|
||||
2026-06-11T15:06:53,549,26.2,39,0,55
|
||||
2026-06-11T15:07:54,565,26.2,39,0,55
|
||||
2026-06-11T15:08:54,533,26.1,39,0,57
|
||||
2026-06-11T15:09:55,526,26.2,40,0,55
|
||||
2026-06-11T15:10:55,505,26.2,39,0,57
|
||||
2026-06-11T15:11:56,526,26.2,39,0,57
|
||||
2026-06-11T15:12:57,526,26.2,39,0,55
|
||||
2026-06-11T15:13:57,549,26.2,39,0,55
|
||||
2026-06-11T15:14:57,549,26.2,39,0,55
|
||||
2026-06-11T15:15:58,539,26.2,39,0,57
|
||||
2026-06-11T15:16:58,556,26.2,39,0,55
|
||||
2026-06-11T15:17:59,545,26.2,39,0,55
|
||||
2026-06-11T15:18:59,565,26.2,40,0,57
|
||||
2026-06-11T15:21:00,517,26.3,39,0,55
|
||||
2026-06-11T15:22:01,565,26.3,39,0,55
|
||||
2026-06-11T15:23:01,533,26.3,39,0,57
|
||||
2026-06-11T15:24:02,533,26.3,40,0,55
|
||||
2026-06-11T15:25:02,546,26.3,40,0,55
|
||||
2026-06-11T15:26:03,523,26.3,39,0,57
|
||||
2026-06-11T15:27:03,533,26.3,39,0,55
|
||||
2026-06-11T15:28:04,549,26.3,39,0,57
|
||||
2026-06-11T15:29:04,545,26.3,39,0,55
|
||||
2026-06-11T15:30:05,549,26.3,39,0,57
|
||||
2026-06-11T15:31:05,575,26.3,39,0,55
|
||||
2026-06-11T15:32:06,539,26.3,39,0,57
|
||||
2026-06-11T15:33:06,565,26.3,39,0,55
|
||||
2026-06-11T15:34:07,526,26.5,39,0,55
|
||||
2026-06-11T15:35:07,575,26.3,39,0,55
|
||||
2026-06-11T15:37:08,565,26.3,39,0,55
|
||||
2026-06-11T15:38:09,549,26.5,39,0,55
|
||||
2026-06-11T15:39:10,533,26.5,40,0,55
|
||||
2026-06-11T15:40:10,565,26.3,40,0,55
|
||||
2026-06-11T15:41:10,565,26.5,39,0,55
|
||||
2026-06-11T15:42:11,549,26.3,40,0,55
|
||||
2026-06-11T15:43:11,529,26.5,39,0,55
|
||||
2026-06-11T15:45:12,581,26.5,39,0,55
|
||||
2026-06-11T15:46:13,596,26.5,40,0,55
|
||||
2026-06-11T15:47:13,546,26.5,39,0,55
|
||||
2026-06-11T15:48:14,575,26.3,39,0,55
|
||||
2026-06-11T15:49:14,589,26.5,40,0,55
|
||||
2026-06-11T15:50:15,540,26.5,39,0,55
|
||||
2026-06-11T15:51:15,596,26.3,40,0,55
|
||||
2026-06-11T15:52:16,529,26.5,40,0,57
|
||||
2026-06-11T15:53:16,540,26.5,39,0,55
|
||||
2026-06-11T15:54:17,575,26.5,39,0,55
|
||||
2026-06-11T15:55:17,545,26.5,39,0,55
|
||||
2026-06-11T15:56:18,581,26.3,39,0,55
|
||||
2026-06-11T15:57:18,545,26.5,39,0,57
|
||||
2026-06-11T15:58:19,545,26.5,40,0,57
|
||||
2026-06-11T15:59:19,529,26.5,40,0,57
|
||||
2026-06-11T16:00:20,546,26.5,40,0,55
|
||||
2026-06-11T16:01:20,546,26.5,39,0,57
|
||||
2026-06-11T16:02:21,556,26.3,39,0,57
|
||||
2026-06-11T16:03:21,545,26.5,39,0,57
|
||||
2026-06-11T16:04:22,556,26.5,38,0,55
|
||||
2026-06-11T16:05:23,575,26.6,39,0,55
|
||||
2026-06-11T16:06:23,565,26.6,38,0,57
|
||||
2026-06-11T16:07:23,629,26.6,39,0,57
|
||||
2026-06-11T16:08:24,549,26.5,39,0,57
|
||||
2026-06-11T16:09:24,556,26.5,39,0,55
|
||||
2026-06-11T16:10:25,570,26.5,40,0,55
|
||||
2026-06-11T16:11:25,565,26.5,39,0,55
|
||||
2026-06-11T16:12:26,575,26.5,38,0,55
|
||||
2026-06-11T16:13:26,615,26.6,39,0,57
|
||||
2026-06-11T16:14:27,556,26.5,39,0,55
|
||||
2026-06-11T16:15:27,570,26.6,39,0,55
|
||||
2026-06-11T16:16:28,575,26.6,39,0,55
|
||||
2026-06-11T16:17:28,556,26.6,39,0,55
|
||||
2026-06-11T16:18:29,575,26.5,40,0,55
|
||||
2026-06-11T16:19:29,565,26.6,39,0,55
|
||||
2026-06-11T16:20:30,556,26.5,39,0,55
|
||||
2026-06-11T16:21:31,565,26.6,40,0,55
|
||||
2026-06-11T16:22:31,533,26.6,39,0,57
|
||||
2026-06-11T16:23:31,575,26.6,40,0,55
|
||||
2026-06-11T16:24:32,570,26.6,39,0,55
|
||||
2026-06-11T16:25:33,556,26.6,39,0,57
|
||||
2026-06-11T16:26:33,545,26.6,39,0,55
|
||||
2026-06-11T16:27:33,589,26.5,39,0,57
|
||||
2026-06-11T16:28:34,549,26.6,39,0,55
|
||||
2026-06-11T16:29:34,581,26.6,39,0,55
|
||||
|
415
report/resources/measures/A3-E6_8A_79_C8_87_25.csv
Normal file
@@ -0,0 +1,415 @@
|
||||
time,co2,temperature,humidity,windows,battery
|
||||
2026-06-11T08:46:55,400,25.1,39,0,36
|
||||
2026-06-11T08:49:55,405,25.1,39,0,36
|
||||
2026-06-11T08:50:55,413,25.1,38,0,36
|
||||
2026-06-11T08:51:56,425,25.1,39,0,36
|
||||
2026-06-11T08:53:57,408,25.2,39,0,36
|
||||
2026-06-11T08:54:57,418,25.2,38,0,36
|
||||
2026-06-11T08:55:58,425,25.2,39,0,36
|
||||
2026-06-11T08:56:58,418,25.2,39,0,36
|
||||
2026-06-11T08:57:59,423,25.2,38,0,36
|
||||
2026-06-11T08:59:00,442,25.2,39,0,36
|
||||
2026-06-11T09:00:00,426,25.2,39,0,36
|
||||
2026-06-11T09:01:01,426,25.1,38,0,33
|
||||
2026-06-11T09:02:01,434,25.3,39,0,36
|
||||
2026-06-11T09:03:01,423,25.3,39,0,36
|
||||
2026-06-11T09:05:02,434,25.3,39,0,36
|
||||
2026-06-11T09:06:03,439,25.3,38,0,36
|
||||
2026-06-11T09:07:04,454,25.3,39,0,36
|
||||
2026-06-11T09:08:04,439,25.3,39,0,36
|
||||
2026-06-11T09:10:05,454,25.3,39,0,36
|
||||
2026-06-11T09:11:05,439,25.5,38,0,36
|
||||
2026-06-11T09:12:06,454,25.3,38,0,36
|
||||
2026-06-11T09:13:06,454,25.5,39,0,36
|
||||
2026-06-11T09:14:07,439,25.3,39,0,36
|
||||
2026-06-11T09:15:07,439,25.3,39,0,36
|
||||
2026-06-11T09:16:08,434,25.3,39,0,36
|
||||
2026-06-11T09:17:09,445,25.3,38,0,36
|
||||
2026-06-11T09:18:09,459,25.3,38,0,36
|
||||
2026-06-11T09:19:09,459,25.2,39,0,36
|
||||
2026-06-11T09:21:10,434,25.3,39,0,36
|
||||
2026-06-11T09:22:11,434,25.2,39,0,36
|
||||
2026-06-11T09:23:12,426,25.2,39,0,36
|
||||
2026-06-11T09:24:12,423,25.2,38,0,36
|
||||
2026-06-11T09:25:12,442,25.2,39,0,36
|
||||
2026-06-11T09:26:13,426,25.2,38,0,36
|
||||
2026-06-11T09:27:13,438,25.1,38,0,36
|
||||
2026-06-11T09:28:14,438,25.1,38,0,36
|
||||
2026-06-11T09:29:15,438,25.1,39,0,36
|
||||
2026-06-11T09:30:15,418,25.2,39,0,36
|
||||
2026-06-11T09:31:16,418,25.1,38,0,36
|
||||
2026-06-11T09:32:16,418,25.2,39,0,36
|
||||
2026-06-11T09:33:17,415,25.1,39,0,36
|
||||
2026-06-11T09:34:17,418,25.1,38,0,36
|
||||
2026-06-11T09:35:18,427,25,39,0,36
|
||||
2026-06-11T09:36:18,413,25.1,39,0,36
|
||||
2026-06-11T09:37:19,415,25,39,0,36
|
||||
2026-06-11T09:39:20,408,25,39,0,36
|
||||
2026-06-11T09:40:20,405,25.1,39,0,36
|
||||
2026-06-11T09:41:20,408,25,38,0,36
|
||||
2026-06-11T09:42:21,422,25,38,0,36
|
||||
2026-06-11T09:43:21,418,25,38,0,36
|
||||
2026-06-11T09:44:22,418,25.1,39,0,36
|
||||
2026-06-11T09:45:23,408,25.1,39,0,36
|
||||
2026-06-11T09:46:23,408,25.1,38,0,36
|
||||
2026-06-11T09:47:24,422,25.1,38,0,36
|
||||
2026-06-11T09:48:24,414,25.1,39,0,36
|
||||
2026-06-11T09:49:24,408,25,38,0,36
|
||||
2026-06-11T09:50:25,414,25.1,39,0,36
|
||||
2026-06-11T09:51:26,405,25,38,0,36
|
||||
2026-06-11T09:52:26,418,25.1,39,0,36
|
||||
2026-06-11T09:53:27,408,25.1,38,0,36
|
||||
2026-06-11T09:54:27,422,25.1,39,0,36
|
||||
2026-06-11T09:55:28,400,25,39,0,36
|
||||
2026-06-11T09:57:28,400,25,38,0,36
|
||||
2026-06-11T10:00:29,414,25,39,0,36
|
||||
2026-06-11T10:01:29,414,25,39,0,36
|
||||
2026-06-11T10:02:30,400,25,38,0,36
|
||||
2026-06-11T10:04:30,420,25,39,0,36
|
||||
2026-06-11T10:05:31,400,25,40,0,36
|
||||
2026-06-11T10:07:31,400,25,39,0,36
|
||||
2026-06-11T10:10:32,414,25,39,0,36
|
||||
2026-06-11T10:11:32,407,25,39,0,36
|
||||
2026-06-11T10:12:33,414,24.8,39,0,36
|
||||
2026-06-11T10:13:33,488,25,38,0,36
|
||||
2026-06-11T10:14:34,424,24.8,39,0,36
|
||||
2026-06-11T10:16:35,414,24.8,39,0,36
|
||||
2026-06-11T10:17:35,488,25,39,0,36
|
||||
2026-06-11T10:18:36,414,24.8,39,0,36
|
||||
2026-06-11T10:19:36,485,24.8,39,0,36
|
||||
2026-06-11T10:20:37,477,25,39,0,36
|
||||
2026-06-11T10:21:37,400,24.8,39,0,36
|
||||
2026-06-11T10:23:38,480,24.8,39,0,36
|
||||
2026-06-11T10:24:38,480,25,39,0,36
|
||||
2026-06-11T10:25:39,409,25,39,0,36
|
||||
2026-06-11T10:26:39,414,25,39,0,36
|
||||
2026-06-11T10:27:40,420,25,39,0,36
|
||||
2026-06-11T10:28:40,420,25,39,0,36
|
||||
2026-06-11T10:29:41,420,24.8,40,0,31
|
||||
2026-06-11T10:30:41,475,25,39,0,36
|
||||
2026-06-11T10:31:42,420,25,39,0,36
|
||||
2026-06-11T10:32:42,420,25,39,0,36
|
||||
2026-06-11T10:33:43,427,25,40,0,36
|
||||
2026-06-11T10:34:43,420,25.1,39,0,36
|
||||
2026-06-11T10:35:44,432,25.1,40,0,36
|
||||
2026-06-11T10:36:44,415,25,39,0,36
|
||||
2026-06-11T10:37:45,436,25.1,40,0,36
|
||||
2026-06-11T10:38:45,417,25.1,39,0,36
|
||||
2026-06-11T10:39:46,432,25.1,39,0,36
|
||||
2026-06-11T10:40:46,436,25.1,39,0,36
|
||||
2026-06-11T10:41:47,441,25,39,0,36
|
||||
2026-06-11T10:42:47,427,25.1,40,0,36
|
||||
2026-06-11T10:43:48,425,25.1,39,0,33
|
||||
2026-06-11T10:44:49,436,25.1,39,0,36
|
||||
2026-06-11T10:45:49,436,25.1,39,0,36
|
||||
2026-06-11T10:46:49,441,25.1,39,0,36
|
||||
2026-06-11T10:47:50,441,25.1,39,0,36
|
||||
2026-06-11T10:48:50,436,25.1,40,0,36
|
||||
2026-06-11T10:49:51,417,25.1,39,0,36
|
||||
2026-06-11T10:50:52,441,25,39,0,36
|
||||
2026-06-11T10:51:52,424,25.1,39,0,36
|
||||
2026-06-11T10:52:52,441,25.1,40,0,36
|
||||
2026-06-11T10:53:53,425,25.1,39,0,36
|
||||
2026-06-11T10:54:54,441,25.1,39,0,36
|
||||
2026-06-11T10:55:54,436,25.1,40,0,36
|
||||
2026-06-11T10:56:54,420,25.1,39,0,36
|
||||
2026-06-11T10:57:55,441,25.1,39,0,36
|
||||
2026-06-11T10:58:55,441,25.1,39,0,36
|
||||
2026-06-11T10:59:56,441,25,39,0,33
|
||||
2026-06-11T11:00:56,427,25.1,39,0,36
|
||||
2026-06-11T11:02:57,441,25.1,40,0,36
|
||||
2026-06-11T11:03:58,420,25.1,40,0,36
|
||||
2026-06-11T11:04:58,428,25.1,40,0,36
|
||||
2026-06-11T11:05:59,428,25.1,39,0,36
|
||||
2026-06-11T11:06:59,445,25.1,39,0,36
|
||||
2026-06-11T11:08:00,441,25.1,39,0,36
|
||||
2026-06-11T11:09:01,436,25.1,39,0,36
|
||||
2026-06-11T11:10:01,436,25.1,39,0,36
|
||||
2026-06-11T11:11:01,445,25.1,39,0,36
|
||||
2026-06-11T11:12:02,445,25.1,39,0,36
|
||||
2026-06-11T11:13:02,445,25.1,39,0,36
|
||||
2026-06-11T11:14:03,445,25.1,39,0,36
|
||||
2026-06-11T11:15:03,441,25.1,39,0,36
|
||||
2026-06-11T11:16:04,441,25.1,40,0,36
|
||||
2026-06-11T11:17:04,428,25.1,39,0,36
|
||||
2026-06-11T11:18:05,441,25.1,39,0,36
|
||||
2026-06-11T11:19:05,441,25.1,40,0,36
|
||||
2026-06-11T11:20:06,428,25.1,39,0,36
|
||||
2026-06-11T11:21:06,432,25.1,39,0,36
|
||||
2026-06-11T11:22:07,441,25.1,39,0,36
|
||||
2026-06-11T11:23:07,441,25.1,39,0,36
|
||||
2026-06-11T11:25:09,441,25.1,39,0,36
|
||||
2026-06-11T11:26:09,436,25.1,39,0,36
|
||||
2026-06-11T11:27:09,436,25.1,39,0,36
|
||||
2026-06-11T11:29:10,459,25.1,39,0,36
|
||||
2026-06-11T11:30:11,459,25.1,39,0,33
|
||||
2026-06-11T11:32:12,441,25.1,38,0,36
|
||||
2026-06-11T11:33:12,455,25.1,39,0,36
|
||||
2026-06-11T11:34:13,432,25.1,40,0,33
|
||||
2026-06-11T11:35:13,420,25.1,39,0,36
|
||||
2026-06-11T11:36:14,432,25.2,38,0,36
|
||||
2026-06-11T11:37:14,436,25.1,39,0,36
|
||||
2026-06-11T11:38:15,427,25.1,39,0,36
|
||||
2026-06-11T11:39:16,432,25.2,39,0,36
|
||||
2026-06-11T11:40:16,441,25.2,39,0,36
|
||||
2026-06-11T11:41:16,432,25.2,39,0,36
|
||||
2026-06-11T11:42:17,432,25.2,38,0,36
|
||||
2026-06-11T11:43:17,455,25.2,39,0,36
|
||||
2026-06-11T11:44:18,441,25.2,39,0,36
|
||||
2026-06-11T11:45:19,441,25.2,39,0,33
|
||||
2026-06-11T11:46:19,441,25.2,38,0,36
|
||||
2026-06-11T11:47:19,459,25.2,39,0,36
|
||||
2026-06-11T11:48:20,450,25.2,39,0,36
|
||||
2026-06-11T11:49:21,450,25.3,38,0,36
|
||||
2026-06-11T11:50:21,464,25.3,40,0,36
|
||||
2026-06-11T11:51:21,428,25.3,39,0,36
|
||||
2026-06-11T11:52:22,450,25.3,39,0,36
|
||||
2026-06-11T11:53:23,450,25.3,39,0,36
|
||||
2026-06-11T11:54:23,450,25.3,39,0,36
|
||||
2026-06-11T11:55:23,456,25.3,39,0,36
|
||||
2026-06-11T11:56:24,456,25.3,39,0,36
|
||||
2026-06-11T11:57:25,456,25.3,40,0,36
|
||||
2026-06-11T11:58:25,447,25.3,38,0,36
|
||||
2026-06-11T11:59:26,482,25.3,39,0,36
|
||||
2026-06-11T12:00:26,482,25.3,39,0,33
|
||||
2026-06-11T12:01:26,461,25.3,39,0,36
|
||||
2026-06-11T12:02:27,470,25.2,39,0,36
|
||||
2026-06-11T12:03:28,465,25.2,39,0,36
|
||||
2026-06-11T12:04:28,461,25.3,39,0,36
|
||||
2026-06-11T12:07:30,470,25.3,39,0,33
|
||||
2026-06-11T12:10:31,465,25.3,39,0,36
|
||||
2026-06-11T12:11:32,470,25.3,39,0,36
|
||||
2026-06-11T12:12:32,470,25.3,39,0,36
|
||||
2026-06-11T12:13:33,470,25.3,39,0,36
|
||||
2026-06-11T12:14:33,470,25.2,39,0,36
|
||||
2026-06-11T12:15:34,470,25.3,39,0,36
|
||||
2026-06-11T12:16:34,470,25.3,40,0,36
|
||||
2026-06-11T12:17:34,454,25.3,39,0,36
|
||||
2026-06-11T12:18:35,465,25.3,39,0,36
|
||||
2026-06-11T12:19:36,470,25.3,39,0,36
|
||||
2026-06-11T12:20:36,461,25.3,38,0,36
|
||||
2026-06-11T12:21:37,478,25.3,38,0,36
|
||||
2026-06-11T12:22:37,478,25.3,38,0,36
|
||||
2026-06-11T12:23:38,485,25.3,39,0,36
|
||||
2026-06-11T12:24:38,465,25.3,38,0,36
|
||||
2026-06-11T12:25:39,482,25.3,38,0,36
|
||||
2026-06-11T12:27:40,470,25.5,39,0,36
|
||||
2026-06-11T12:28:40,473,25.5,38,0,36
|
||||
2026-06-11T12:30:41,473,25.3,39,0,33
|
||||
2026-06-11T12:31:42,473,25.5,38,0,36
|
||||
2026-06-11T12:32:42,485,25.3,38,0,36
|
||||
2026-06-11T12:33:43,490,25.5,38,0,36
|
||||
2026-06-11T12:34:43,485,25.5,39,0,36
|
||||
2026-06-11T12:35:44,473,25.5,38,0,36
|
||||
2026-06-11T12:36:44,490,25.5,39,0,36
|
||||
2026-06-11T12:37:45,473,25.5,39,0,36
|
||||
2026-06-11T12:38:45,478,25.5,38,0,36
|
||||
2026-06-11T12:39:46,490,25.5,39,0,36
|
||||
2026-06-11T12:40:46,473,25.5,38,0,33
|
||||
2026-06-11T12:41:47,493,25.5,39,0,33
|
||||
2026-06-11T12:42:47,473,25.6,39,0,36
|
||||
2026-06-11T12:43:48,478,25.5,38,0,33
|
||||
2026-06-11T12:44:48,493,25.5,38,0,36
|
||||
2026-06-11T12:45:49,493,25.5,39,0,33
|
||||
2026-06-11T12:46:49,490,25.6,38,0,36
|
||||
2026-06-11T12:47:50,493,25.6,39,0,36
|
||||
2026-06-11T12:48:50,481,25.6,38,0,36
|
||||
2026-06-11T12:49:51,493,25.6,38,0,36
|
||||
2026-06-11T12:50:51,496,25.6,38,0,36
|
||||
2026-06-11T12:51:52,505,25.6,38,0,36
|
||||
2026-06-11T12:52:52,505,25.6,38,0,36
|
||||
2026-06-11T12:53:53,505,25.6,38,0,33
|
||||
2026-06-11T12:54:53,496,25.6,38,0,36
|
||||
2026-06-11T12:55:54,496,25.6,38,0,36
|
||||
2026-06-11T12:56:54,505,25.7,38,0,33
|
||||
2026-06-11T12:57:55,505,25.6,39,0,33
|
||||
2026-06-11T12:58:55,492,25.6,39,0,36
|
||||
2026-06-11T12:59:56,490,25.6,38,0,36
|
||||
2026-06-11T13:00:56,505,25.7,39,0,36
|
||||
2026-06-11T13:01:57,505,25.6,38,0,33
|
||||
2026-06-11T13:02:57,507,25.6,39,0,33
|
||||
2026-06-11T13:03:58,492,25.6,38,0,33
|
||||
2026-06-11T13:04:58,507,25.7,39,0,33
|
||||
2026-06-11T13:05:59,492,25.7,38,0,33
|
||||
2026-06-11T13:06:59,507,25.7,39,0,36
|
||||
2026-06-11T13:08:00,481,25.7,38,0,36
|
||||
2026-06-11T13:09:00,505,25.7,39,0,36
|
||||
2026-06-11T13:10:01,492,25.7,39,0,33
|
||||
2026-06-11T13:11:02,492,25.7,38,0,33
|
||||
2026-06-11T13:12:02,507,25.7,38,0,36
|
||||
2026-06-11T13:13:02,521,25.6,38,0,36
|
||||
2026-06-11T13:14:03,521,25.7,38,0,36
|
||||
2026-06-11T13:15:03,521,25.7,39,0,36
|
||||
2026-06-11T13:16:04,501,25.7,38,0,33
|
||||
2026-06-11T13:17:04,517,25.7,38,0,33
|
||||
2026-06-11T13:18:05,530,25.7,39,0,36
|
||||
2026-06-11T13:19:05,508,25.7,39,0,33
|
||||
2026-06-11T13:20:06,508,25.7,39,0,33
|
||||
2026-06-11T13:21:06,518,25.8,39,0,36
|
||||
2026-06-11T13:22:07,523,25.8,38,0,36
|
||||
2026-06-11T13:23:07,530,25.8,39,0,33
|
||||
2026-06-11T13:24:08,523,25.8,38,0,36
|
||||
2026-06-11T13:25:09,542,25.8,38,0,33
|
||||
2026-06-11T13:26:09,542,25.8,39,0,33
|
||||
2026-06-11T13:27:09,526,25.8,38,0,33
|
||||
2026-06-11T13:28:10,542,26,39,0,36
|
||||
2026-06-11T13:29:11,441,26,38,0,36
|
||||
2026-06-11T13:30:11,455,26,39,0,33
|
||||
2026-06-11T13:31:11,451,26,38,0,33
|
||||
2026-06-11T13:32:12,460,26,39,0,36
|
||||
2026-06-11T13:33:13,441,26,38,0,36
|
||||
2026-06-11T13:34:13,441,26,39,0,36
|
||||
2026-06-11T13:35:13,441,26,38,0,36
|
||||
2026-06-11T13:36:14,465,26,38,0,33
|
||||
2026-06-11T13:37:14,460,26,39,0,36
|
||||
2026-06-11T13:38:15,441,26,38,0,33
|
||||
2026-06-11T13:39:16,455,26.1,39,0,33
|
||||
2026-06-11T13:40:16,451,26,38,0,36
|
||||
2026-06-11T13:41:16,465,26.1,39,0,33
|
||||
2026-06-11T13:42:17,451,26,38,0,36
|
||||
2026-06-11T13:43:18,465,26,38,0,33
|
||||
2026-06-11T13:44:18,460,26.1,38,0,33
|
||||
2026-06-11T13:45:18,465,26,38,0,33
|
||||
2026-06-11T13:46:19,465,26.1,39,0,36
|
||||
2026-06-11T13:47:19,451,26,38,0,36
|
||||
2026-06-11T13:48:20,465,26.1,38,0,33
|
||||
2026-06-11T13:49:20,465,26,39,0,36
|
||||
2026-06-11T13:50:21,446,26,38,0,36
|
||||
2026-06-11T13:51:22,460,26,38,0,33
|
||||
2026-06-11T13:52:22,460,26,38,0,36
|
||||
2026-06-11T13:53:22,460,26,39,0,36
|
||||
2026-06-11T13:54:23,446,26,38,0,33
|
||||
2026-06-11T13:55:23,460,26,39,0,36
|
||||
2026-06-11T13:56:24,438,26,39,0,36
|
||||
2026-06-11T13:57:24,438,26,38,0,33
|
||||
2026-06-11T13:58:25,455,26,38,0,33
|
||||
2026-06-11T13:59:25,460,26,39,0,33
|
||||
2026-06-11T14:01:26,438,26,39,0,36
|
||||
2026-06-11T14:02:27,438,26,37,0,36
|
||||
2026-06-11T14:03:28,470,26.1,38,0,33
|
||||
2026-06-11T14:04:28,455,26,38,0,36
|
||||
2026-06-11T14:05:29,482,26,38,0,36
|
||||
2026-06-11T14:08:30,452,26,38,0,36
|
||||
2026-06-11T14:09:31,455,26.1,38,0,36
|
||||
2026-06-11T14:10:31,455,26.1,38,0,33
|
||||
2026-06-11T14:11:32,452,26.1,39,0,36
|
||||
2026-06-11T14:12:32,441,26.1,39,0,36
|
||||
2026-06-11T14:14:33,452,26.1,38,0,36
|
||||
2026-06-11T14:15:34,455,26.1,38,0,33
|
||||
2026-06-11T14:17:34,460,26.1,38,0,33
|
||||
2026-06-11T14:19:36,455,26.1,38,0,33
|
||||
2026-06-11T14:20:36,452,26,38,0,33
|
||||
2026-06-11T14:21:36,455,26.1,38,0,36
|
||||
2026-06-11T14:22:37,455,26,38,0,36
|
||||
2026-06-11T14:23:38,452,26.1,38,0,36
|
||||
2026-06-11T14:24:38,460,26.1,38,0,36
|
||||
2026-06-11T14:25:39,465,26,37,0,36
|
||||
2026-06-11T14:26:39,495,26.1,38,0,33
|
||||
2026-06-11T14:27:40,475,26.1,38,0,36
|
||||
2026-06-11T14:28:40,475,26.1,38,0,36
|
||||
2026-06-11T14:29:40,471,26.1,38,0,33
|
||||
2026-06-11T14:30:41,475,26.2,38,0,36
|
||||
2026-06-11T14:31:41,475,26.1,39,0,36
|
||||
2026-06-11T14:32:42,459,26.1,37,0,33
|
||||
2026-06-11T14:33:43,492,26.2,38,0,36
|
||||
2026-06-11T14:34:43,471,26.1,38,0,33
|
||||
2026-06-11T14:35:44,475,26.1,38,0,33
|
||||
2026-06-11T14:36:44,475,26.2,38,0,36
|
||||
2026-06-11T14:37:45,475,26.1,38,0,33
|
||||
2026-06-11T14:38:45,475,26.1,38,0,33
|
||||
2026-06-11T14:39:46,471,26.2,37,0,33
|
||||
2026-06-11T14:40:46,492,26.2,38,0,33
|
||||
2026-06-11T14:41:47,465,26.1,37,0,36
|
||||
2026-06-11T14:42:47,488,26.1,38,0,36
|
||||
2026-06-11T14:43:48,475,26.2,38,0,36
|
||||
2026-06-11T14:44:48,465,26.2,38,0,36
|
||||
2026-06-11T14:45:49,465,26.2,38,0,36
|
||||
2026-06-11T14:47:50,465,26.2,38,0,36
|
||||
2026-06-11T14:48:50,475,26.2,38,0,33
|
||||
2026-06-11T14:50:51,488,26.2,37,0,33
|
||||
2026-06-11T14:52:52,475,26.2,38,0,33
|
||||
2026-06-11T14:53:53,471,26.2,37,0,36
|
||||
2026-06-11T14:54:53,488,26.2,38,0,33
|
||||
2026-06-11T14:55:54,471,26.2,37,0,33
|
||||
2026-06-11T14:56:54,483,26.2,38,0,33
|
||||
2026-06-11T14:57:55,471,26.3,38,0,36
|
||||
2026-06-11T14:58:55,475,26.3,38,0,33
|
||||
2026-06-11T14:59:56,475,26.3,37,0,33
|
||||
2026-06-11T15:00:56,488,26.3,38,0,33
|
||||
2026-06-11T15:01:57,475,26.3,38,0,36
|
||||
2026-06-11T15:02:57,475,26.3,37,0,36
|
||||
2026-06-11T15:03:58,500,26.2,37,0,36
|
||||
2026-06-11T15:04:58,495,26.2,37,0,33
|
||||
2026-06-11T15:05:59,506,26.3,37,0,36
|
||||
2026-06-11T15:06:59,515,26.3,38,0,33
|
||||
2026-06-11T15:08:00,493,26.3,38,0,36
|
||||
2026-06-11T15:09:00,497,26.3,38,0,36
|
||||
2026-06-11T15:10:01,493,26.3,39,0,33
|
||||
2026-06-11T15:11:01,493,26.5,37,0,33
|
||||
2026-06-11T15:12:02,523,26.5,38,0,33
|
||||
2026-06-11T15:13:02,506,26.5,38,0,33
|
||||
2026-06-11T15:14:03,506,26.5,38,0,33
|
||||
2026-06-11T15:15:03,506,26.5,38,0,33
|
||||
2026-06-11T15:16:04,521,26.5,38,0,36
|
||||
2026-06-11T15:17:04,526,26.5,37,0,36
|
||||
2026-06-11T15:18:05,544,26.5,38,0,33
|
||||
2026-06-11T15:21:07,529,26.3,38,0,33
|
||||
2026-06-11T15:22:07,526,26.5,38,0,33
|
||||
2026-06-11T15:23:07,529,26.5,38,0,36
|
||||
2026-06-11T15:25:08,518,26.6,38,0,33
|
||||
2026-06-11T15:26:09,538,26.5,38,0,36
|
||||
2026-06-11T15:28:10,541,26.5,37,0,33
|
||||
2026-06-11T15:29:10,556,26.5,39,0,33
|
||||
2026-06-11T15:31:12,541,26.5,38,0,36
|
||||
2026-06-11T15:33:13,541,26.5,38,0,36
|
||||
2026-06-11T15:34:13,541,26.6,38,0,36
|
||||
2026-06-11T15:35:13,546,26.5,38,0,33
|
||||
2026-06-11T15:36:14,541,26.5,38,0,33
|
||||
2026-06-11T15:38:15,529,26.6,38,0,33
|
||||
2026-06-11T15:39:15,546,26.5,38,0,33
|
||||
2026-06-11T15:40:16,546,26.5,37,0,36
|
||||
2026-06-11T15:41:16,566,26.5,38,0,36
|
||||
2026-06-11T15:42:17,541,26.5,39,0,36
|
||||
2026-06-11T15:44:18,534,26.5,39,0,31
|
||||
2026-06-11T15:45:18,524,26.5,39,0,33
|
||||
2026-06-11T15:46:19,524,26.6,38,0,33
|
||||
2026-06-11T15:47:20,551,26.5,38,0,36
|
||||
2026-06-11T15:48:20,546,26.5,38,0,33
|
||||
2026-06-11T15:49:20,541,26.5,38,0,33
|
||||
2026-06-11T15:50:21,546,26.3,38,0,33
|
||||
2026-06-11T15:51:22,541,26.5,38,0,33
|
||||
2026-06-11T15:53:23,546,26.5,38,0,33
|
||||
2026-06-11T15:54:23,541,26.5,38,0,33
|
||||
2026-06-11T15:55:23,546,26.5,39,0,33
|
||||
2026-06-11T15:56:24,534,26.6,38,0,33
|
||||
2026-06-11T15:57:24,551,26.6,38,0,33
|
||||
2026-06-11T15:58:25,551,26.6,37,0,33
|
||||
2026-06-11T15:59:25,578,26.6,37,0,33
|
||||
2026-06-11T16:00:26,566,26.6,39,0,36
|
||||
2026-06-11T16:01:26,534,26.5,39,0,33
|
||||
2026-06-11T16:02:27,529,26.6,37,0,36
|
||||
2026-06-11T16:03:27,566,26.6,38,0,33
|
||||
2026-06-11T16:04:28,551,26.6,39,0,33
|
||||
2026-06-11T16:05:29,534,26.6,39,0,33
|
||||
2026-06-11T16:06:29,540,26.6,38,0,33
|
||||
2026-06-11T16:07:29,551,26.6,37,0,36
|
||||
2026-06-11T16:08:30,571,26.6,38,0,33
|
||||
2026-06-11T16:09:31,556,26.6,38,0,36
|
||||
2026-06-11T16:10:31,546,26.6,38,0,33
|
||||
2026-06-11T16:11:31,551,26.5,38,0,33
|
||||
2026-06-11T16:12:32,551,26.6,38,0,36
|
||||
2026-06-11T16:13:32,551,26.6,39,0,36
|
||||
2026-06-11T16:14:33,529,26.6,38,0,33
|
||||
2026-06-11T16:15:33,551,26.6,37,0,36
|
||||
2026-06-11T16:16:34,571,26.6,38,0,33
|
||||
2026-06-11T16:17:34,571,26.6,39,0,31
|
||||
2026-06-11T16:18:35,529,26.6,37,0,33
|
||||
2026-06-11T16:19:35,566,26.6,38,0,36
|
||||
2026-06-11T16:22:37,551,26.6,38,0,33
|
||||
2026-06-11T16:23:38,556,26.6,38,0,33
|
||||
2026-06-11T16:24:38,562,26.6,39,0,33
|
||||
2026-06-11T16:25:38,534,26.6,38,0,33
|
||||
2026-06-11T16:26:39,562,26.6,39,0,33
|
||||
2026-06-11T16:27:40,540,26.6,38,0,33
|
||||
2026-06-11T16:28:40,551,26.6,39,0,33
|
||||
2026-06-11T16:29:41,542,26.6,38,0,33
|
||||
|
441
report/resources/measures/A3-ED_B2_F3_74_3E_C2.csv
Normal file
@@ -0,0 +1,441 @@
|
||||
time,co2,temperature,humidity,windows,battery
|
||||
2026-06-11T08:45:47,552,25,40,0,31
|
||||
2026-06-11T08:46:47,485,25,41,0,31
|
||||
2026-06-11T08:47:48,463,25,40,0,31
|
||||
2026-06-11T08:48:48,491,25,41,0,31
|
||||
2026-06-11T08:49:49,463,25,41,0,31
|
||||
2026-06-11T08:50:49,463,25.1,42,0,31
|
||||
2026-06-11T08:51:50,461,25.1,40,0,31
|
||||
2026-06-11T08:52:51,493,25.1,41,0,31
|
||||
2026-06-11T08:53:51,478,25.1,40,0,31
|
||||
2026-06-11T08:54:52,493,25.2,40,0,31
|
||||
2026-06-11T08:55:52,493,25.2,40,0,31
|
||||
2026-06-11T08:56:53,501,25.2,40,0,31
|
||||
2026-06-11T08:57:53,514,25.2,41,0,31
|
||||
2026-06-11T08:59:54,492,25.2,40,0,31
|
||||
2026-06-11T09:00:55,514,25.2,41,0,31
|
||||
2026-06-11T09:01:55,500,25.2,41,0,31
|
||||
2026-06-11T09:02:56,508,25.2,41,0,31
|
||||
2026-06-11T09:03:56,495,25.3,41,0,31
|
||||
2026-06-11T09:04:57,508,25.3,40,0,31
|
||||
2026-06-11T09:05:57,529,25.3,40,0,31
|
||||
2026-06-11T09:06:58,529,25.3,41,0,31
|
||||
2026-06-11T09:07:58,515,25.3,40,0,31
|
||||
2026-06-11T09:08:59,537,25.3,41,0,31
|
||||
2026-06-11T09:09:59,520,25.3,41,0,31
|
||||
2026-06-11T09:11:00,520,25.5,42,0,31
|
||||
2026-06-11T09:12:00,497,25.3,41,0,31
|
||||
2026-06-11T09:13:01,527,25.3,41,0,31
|
||||
2026-06-11T09:14:01,520,25.3,41,0,31
|
||||
2026-06-11T09:15:02,515,25.3,41,0,31
|
||||
2026-06-11T09:16:02,520,25.3,40,0,31
|
||||
2026-06-11T09:17:03,537,25.3,40,0,31
|
||||
2026-06-11T09:18:03,537,25.3,40,0,31
|
||||
2026-06-11T09:20:04,531,25.2,41,0,31
|
||||
2026-06-11T09:22:05,529,25.2,41,0,31
|
||||
2026-06-11T09:23:06,500,25.2,40,0,31
|
||||
2026-06-11T09:24:06,514,25.2,41,0,31
|
||||
2026-06-11T09:25:07,495,25.2,41,0,31
|
||||
2026-06-11T09:26:07,492,25.2,41,0,31
|
||||
2026-06-11T09:27:08,486,25.2,41,0,31
|
||||
2026-06-11T09:28:08,486,25.2,40,0,31
|
||||
2026-06-11T09:29:09,506,25.2,40,0,31
|
||||
2026-06-11T09:30:09,501,25.2,40,0,31
|
||||
2026-06-11T09:31:10,501,25.1,40,0,31
|
||||
2026-06-11T09:32:10,501,25.1,39,0,31
|
||||
2026-06-11T09:33:11,508,25.2,40,0,31
|
||||
2026-06-11T09:34:11,501,25.1,40,0,31
|
||||
2026-06-11T09:35:12,501,25.1,40,0,31
|
||||
2026-06-11T09:36:12,491,25.1,40,0,31
|
||||
2026-06-11T09:37:13,493,25.1,40,0,29
|
||||
2026-06-11T09:38:13,493,25.1,40,0,31
|
||||
2026-06-11T09:40:14,493,25.1,40,0,29
|
||||
2026-06-11T09:41:15,485,25.1,40,0,31
|
||||
2026-06-11T09:42:15,491,25.1,40,0,31
|
||||
2026-06-11T09:43:16,493,25.1,40,0,31
|
||||
2026-06-11T09:44:16,493,25.1,40,0,31
|
||||
2026-06-11T09:45:17,491,25.1,41,0,31
|
||||
2026-06-11T09:46:17,478,25.1,41,0,31
|
||||
2026-06-11T09:47:18,478,25.1,40,0,29
|
||||
2026-06-11T09:48:18,493,25.1,40,0,31
|
||||
2026-06-11T09:49:19,485,25.1,41,0,29
|
||||
2026-06-11T09:50:19,476,25.1,39,0,29
|
||||
2026-06-11T09:51:20,500,25.1,40,0,31
|
||||
2026-06-11T09:52:20,485,25.1,41,0,31
|
||||
2026-06-11T09:53:21,476,25.1,40,0,31
|
||||
2026-06-11T09:54:21,491,25.1,40,0,31
|
||||
2026-06-11T09:55:22,491,25.1,40,0,31
|
||||
2026-06-11T09:56:22,493,25.1,40,0,31
|
||||
2026-06-11T09:57:23,491,25.1,41,0,31
|
||||
2026-06-11T09:58:23,470,25.1,40,0,31
|
||||
2026-06-11T09:59:24,491,25.1,41,0,31
|
||||
2026-06-11T10:00:25,476,25.1,41,0,31
|
||||
2026-06-11T10:01:25,476,25.1,41,0,31
|
||||
2026-06-11T10:03:26,470,25.1,41,0,31
|
||||
2026-06-11T10:04:26,470,25.1,41,0,31
|
||||
2026-06-11T10:05:27,476,25.1,41,0,31
|
||||
2026-06-11T10:06:28,476,25.2,41,0,31
|
||||
2026-06-11T10:07:28,470,25.1,40,0,31
|
||||
2026-06-11T10:08:28,485,25.1,40,0,31
|
||||
2026-06-11T10:09:29,491,25.1,40,0,31
|
||||
2026-06-11T10:10:29,491,25.1,41,0,31
|
||||
2026-06-11T10:11:30,476,25.1,41,0,31
|
||||
2026-06-11T10:12:30,476,25,41,0,31
|
||||
2026-06-11T10:13:31,476,25.1,40,0,33
|
||||
2026-06-11T10:14:31,493,25.1,41,0,31
|
||||
2026-06-11T10:15:32,470,25.1,41,0,31
|
||||
2026-06-11T10:16:32,476,25.1,40,0,31
|
||||
2026-06-11T10:17:33,485,25,41,0,31
|
||||
2026-06-11T10:18:34,478,25,41,0,31
|
||||
2026-06-11T10:19:34,454,25,41,0,31
|
||||
2026-06-11T10:20:34,463,25.1,41,0,31
|
||||
2026-06-11T10:21:35,454,25,40,0,31
|
||||
2026-06-11T10:22:36,466,25.1,41,0,31
|
||||
2026-06-11T10:23:36,470,25.1,41,0,31
|
||||
2026-06-11T10:24:36,463,25.1,41,0,31
|
||||
2026-06-11T10:25:37,478,25.1,41,0,31
|
||||
2026-06-11T10:26:37,478,25.1,41,0,31
|
||||
2026-06-11T10:28:38,492,25.1,41,0,31
|
||||
2026-06-11T10:29:39,486,25.1,41,0,29
|
||||
2026-06-11T10:30:39,486,25.2,41,0,29
|
||||
2026-06-11T10:31:40,492,25.2,40,0,31
|
||||
2026-06-11T10:32:40,506,25.2,41,0,29
|
||||
2026-06-11T10:33:41,495,25.2,41,0,31
|
||||
2026-06-11T10:34:41,495,25.2,42,0,31
|
||||
2026-06-11T10:36:42,519,25.2,41,0,31
|
||||
2026-06-11T10:37:43,508,25.2,41,0,31
|
||||
2026-06-11T10:38:44,508,25.2,41,0,29
|
||||
2026-06-11T10:39:44,515,25.2,41,0,31
|
||||
2026-06-11T10:40:44,500,25.2,41,0,31
|
||||
2026-06-11T10:41:45,508,25.2,41,0,31
|
||||
2026-06-11T10:42:45,500,25.2,41,0,31
|
||||
2026-06-11T10:43:46,508,25.1,41,0,29
|
||||
2026-06-11T10:44:46,508,25.2,41,0,31
|
||||
2026-06-11T10:45:47,500,25.2,41,0,31
|
||||
2026-06-11T10:46:47,508,25.2,42,0,31
|
||||
2026-06-11T10:47:48,486,25.2,42,0,31
|
||||
2026-06-11T10:48:48,486,25.2,40,0,29
|
||||
2026-06-11T10:49:49,519,25.1,40,0,31
|
||||
2026-06-11T10:50:50,519,25.2,42,0,31
|
||||
2026-06-11T10:51:50,486,25.2,42,0,31
|
||||
2026-06-11T10:52:51,486,25.1,40,0,31
|
||||
2026-06-11T10:53:51,514,25.1,40,0,29
|
||||
2026-06-11T10:54:52,514,25.2,41,0,31
|
||||
2026-06-11T10:55:52,508,25.1,42,0,31
|
||||
2026-06-11T10:56:53,495,25.1,41,0,31
|
||||
2026-06-11T10:57:53,508,25.2,41,0,29
|
||||
2026-06-11T10:58:54,500,25.1,41,0,29
|
||||
2026-06-11T10:59:54,500,25.1,41,0,31
|
||||
2026-06-11T11:00:55,500,25.1,41,0,31
|
||||
2026-06-11T11:01:55,508,25.2,42,0,31
|
||||
2026-06-11T11:02:55,486,25.1,41,0,31
|
||||
2026-06-11T11:03:56,500,25.1,42,0,31
|
||||
2026-06-11T11:04:57,495,25.2,42,0,31
|
||||
2026-06-11T11:05:57,495,25.2,41,0,29
|
||||
2026-06-11T11:06:57,508,25.2,41,0,29
|
||||
2026-06-11T11:07:58,508,25.2,42,0,31
|
||||
2026-06-11T11:08:59,486,25.2,41,0,29
|
||||
2026-06-11T11:09:59,508,25.2,42,0,29
|
||||
2026-06-11T11:11:00,486,25.2,42,0,31
|
||||
2026-06-11T11:12:00,495,25.2,41,0,31
|
||||
2026-06-11T11:13:01,500,25.2,40,0,29
|
||||
2026-06-11T11:14:01,531,25.2,40,0,31
|
||||
2026-06-11T11:15:02,519,25.1,41,0,31
|
||||
2026-06-11T11:16:02,508,25.2,41,0,31
|
||||
2026-06-11T11:17:03,508,25.2,41,0,29
|
||||
2026-06-11T11:18:03,500,25.2,41,0,31
|
||||
2026-06-11T11:19:04,500,25.2,42,0,29
|
||||
2026-06-11T11:20:04,497,25.2,41,0,31
|
||||
2026-06-11T11:21:05,508,25.2,41,0,31
|
||||
2026-06-11T11:22:05,500,25.2,41,0,31
|
||||
2026-06-11T11:23:06,515,25.2,41,0,31
|
||||
2026-06-11T11:24:06,508,25.2,41,0,31
|
||||
2026-06-11T11:25:07,500,25.2,40,0,31
|
||||
2026-06-11T11:26:07,531,25.2,41,0,31
|
||||
2026-06-11T11:27:08,508,25.2,40,0,29
|
||||
2026-06-11T11:28:08,529,25.2,41,0,31
|
||||
2026-06-11T11:29:09,500,25.2,41,0,31
|
||||
2026-06-11T11:30:09,500,25.2,40,0,29
|
||||
2026-06-11T11:31:10,531,25.2,41,0,29
|
||||
2026-06-11T11:32:10,508,25.2,41,0,31
|
||||
2026-06-11T11:33:11,508,25.2,41,0,31
|
||||
2026-06-11T11:34:11,500,25.2,41,0,31
|
||||
2026-06-11T11:35:12,508,25.2,40,0,31
|
||||
2026-06-11T11:36:12,514,25.2,41,0,31
|
||||
2026-06-11T11:37:13,486,25.2,40,0,29
|
||||
2026-06-11T11:38:13,501,25.2,41,0,31
|
||||
2026-06-11T11:39:14,495,25.2,42,0,29
|
||||
2026-06-11T11:40:14,495,25.2,39,0,29
|
||||
2026-06-11T11:41:15,545,25.2,40,0,29
|
||||
2026-06-11T11:42:15,531,25.2,41,0,31
|
||||
2026-06-11T11:43:16,508,25.3,41,0,29
|
||||
2026-06-11T11:44:16,520,25.3,42,0,31
|
||||
2026-06-11T11:45:17,502,25.3,41,0,31
|
||||
2026-06-11T11:47:18,520,25.5,41,0,31
|
||||
2026-06-11T11:48:18,536,25.3,40,0,31
|
||||
2026-06-11T11:49:19,543,25.5,40,0,31
|
||||
2026-06-11T11:50:19,555,25.3,41,0,29
|
||||
2026-06-11T11:51:20,536,25.3,42,0,29
|
||||
2026-06-11T11:52:20,508,25.3,40,0,31
|
||||
2026-06-11T11:54:21,520,25.3,40,0,29
|
||||
2026-06-11T11:56:22,555,25.5,41,0,31
|
||||
2026-06-11T11:57:23,539,25.5,40,0,31
|
||||
2026-06-11T11:58:23,564,25.5,41,0,31
|
||||
2026-06-11T11:59:24,551,25.5,41,0,29
|
||||
2026-06-11T12:00:24,556,25.3,40,0,31
|
||||
2026-06-11T12:02:25,556,25.5,42,0,31
|
||||
2026-06-11T12:04:26,551,25.5,41,0,29
|
||||
2026-06-11T12:05:27,551,25.5,41,0,29
|
||||
2026-06-11T12:06:27,551,25.3,41,0,29
|
||||
2026-06-11T12:07:28,556,25.3,41,0,31
|
||||
2026-06-11T12:08:28,551,25.5,40,0,31
|
||||
2026-06-11T12:09:29,575,25.5,41,0,31
|
||||
2026-06-11T12:10:29,551,25.5,41,0,31
|
||||
2026-06-11T12:11:30,551,25.3,41,0,31
|
||||
2026-06-11T12:12:30,545,25.5,41,0,31
|
||||
2026-06-11T12:13:31,551,25.5,42,0,29
|
||||
2026-06-11T12:15:32,530,25.3,42,0,29
|
||||
2026-06-11T12:16:32,536,25.5,42,0,29
|
||||
2026-06-11T12:17:33,536,25.5,42,0,29
|
||||
2026-06-11T12:18:33,530,25.5,41,0,31
|
||||
2026-06-11T12:19:34,551,25.5,40,0,31
|
||||
2026-06-11T12:20:34,564,25.5,41,0,29
|
||||
2026-06-11T12:21:35,539,25.5,41,0,31
|
||||
2026-06-11T12:22:35,545,25.5,40,0,29
|
||||
2026-06-11T12:23:36,569,25.5,40,0,29
|
||||
2026-06-11T12:24:36,575,25.5,42,0,31
|
||||
2026-06-11T12:25:37,530,25.5,40,0,29
|
||||
2026-06-11T12:26:37,569,25.5,41,0,31
|
||||
2026-06-11T12:27:38,551,25.5,40,0,29
|
||||
2026-06-11T12:28:38,564,25.5,40,0,29
|
||||
2026-06-11T12:29:39,564,25.5,41,0,31
|
||||
2026-06-11T12:30:39,545,25.5,40,0,29
|
||||
2026-06-11T12:31:40,569,25.5,39,0,29
|
||||
2026-06-11T12:32:40,588,25.6,42,0,31
|
||||
2026-06-11T12:33:41,536,25.5,39,0,29
|
||||
2026-06-11T12:34:41,588,25.5,40,0,29
|
||||
2026-06-11T12:35:42,575,25.6,40,0,31
|
||||
2026-06-11T12:36:43,569,25.5,41,0,29
|
||||
2026-06-11T12:37:43,556,25.6,41,0,29
|
||||
2026-06-11T12:38:44,556,25.5,41,0,29
|
||||
2026-06-11T12:39:44,551,25.6,40,0,31
|
||||
2026-06-11T12:40:44,575,25.6,40,0,29
|
||||
2026-06-11T12:41:45,569,25.6,42,0,31
|
||||
2026-06-11T12:42:45,548,25.6,39,0,29
|
||||
2026-06-11T12:43:46,594,25.5,41,0,31
|
||||
2026-06-11T12:44:46,545,25.6,42,0,29
|
||||
2026-06-11T12:45:47,548,25.6,41,0,29
|
||||
2026-06-11T12:46:47,565,25.6,40,0,31
|
||||
2026-06-11T12:47:48,575,25.6,42,0,31
|
||||
2026-06-11T12:48:48,553,25.6,40,0,31
|
||||
2026-06-11T12:49:49,581,25.6,39,0,29
|
||||
2026-06-11T12:50:49,594,25.6,39,0,29
|
||||
2026-06-11T12:51:50,607,25.6,40,0,29
|
||||
2026-06-11T12:52:50,589,25.6,41,0,29
|
||||
2026-06-11T12:53:51,556,25.6,42,0,29
|
||||
2026-06-11T12:54:51,553,25.6,42,0,29
|
||||
2026-06-11T12:55:52,548,25.6,40,0,29
|
||||
2026-06-11T12:56:53,589,25.6,39,0,29
|
||||
2026-06-11T12:57:53,607,25.7,41,0,29
|
||||
2026-06-11T12:58:53,575,25.6,40,0,29
|
||||
2026-06-11T12:59:54,589,25.6,40,0,29
|
||||
2026-06-11T13:00:54,594,25.7,40,0,29
|
||||
2026-06-11T13:01:55,602,25.7,40,0,29
|
||||
2026-06-11T13:02:55,589,25.7,39,0,29
|
||||
2026-06-11T13:03:56,623,25.7,39,0,29
|
||||
2026-06-11T13:04:56,614,25.7,41,0,29
|
||||
2026-06-11T13:05:57,575,25.7,40,0,29
|
||||
2026-06-11T13:06:58,589,25.7,41,0,29
|
||||
2026-06-11T13:07:58,575,25.7,38,0,29
|
||||
2026-06-11T13:08:58,627,25.7,40,0,29
|
||||
2026-06-11T13:09:59,611,25.7,40,0,29
|
||||
2026-06-11T13:10:59,602,25.6,40,0,29
|
||||
2026-06-11T13:12:00,602,25.7,40,0,29
|
||||
2026-06-11T13:13:00,611,25.7,42,0,29
|
||||
2026-06-11T13:14:01,567,25.7,40,0,29
|
||||
2026-06-11T13:15:01,594,25.7,42,0,31
|
||||
2026-06-11T13:16:02,574,25.7,40,0,29
|
||||
2026-06-11T13:17:03,589,25.7,41,0,29
|
||||
2026-06-11T13:18:03,584,25.8,40,0,29
|
||||
2026-06-11T13:19:04,589,25.7,40,0,29
|
||||
2026-06-11T13:20:04,602,25.8,41,0,29
|
||||
2026-06-11T13:21:05,584,25.8,40,0,29
|
||||
2026-06-11T13:22:05,594,25.8,41,0,31
|
||||
2026-06-11T13:24:06,560,25.8,42,0,29
|
||||
2026-06-11T13:25:07,574,25.8,38,0,29
|
||||
2026-06-11T13:26:07,641,26,41,0,29
|
||||
2026-06-11T13:27:08,531,25.8,40,0,29
|
||||
2026-06-11T13:28:08,611,25.8,41,0,29
|
||||
2026-06-11T13:29:09,575,25.8,42,0,29
|
||||
2026-06-11T13:30:09,574,26,40,0,29
|
||||
2026-06-11T13:31:10,548,26,41,0,29
|
||||
2026-06-11T13:32:10,526,26,41,0,29
|
||||
2026-06-11T13:33:11,531,26,40,0,29
|
||||
2026-06-11T13:34:11,548,26,42,0,29
|
||||
2026-06-11T13:35:12,517,26,41,0,29
|
||||
2026-06-11T13:36:12,531,26,41,0,29
|
||||
2026-06-11T13:37:13,531,26,40,0,29
|
||||
2026-06-11T13:38:13,548,25.8,42,0,29
|
||||
2026-06-11T13:39:14,567,26,42,0,29
|
||||
2026-06-11T13:40:14,517,26,42,0,29
|
||||
2026-06-11T13:41:15,501,26,41,0,29
|
||||
2026-06-11T13:42:15,526,26,39,0,29
|
||||
2026-06-11T13:43:16,560,26,41,0,29
|
||||
2026-06-11T13:44:16,526,26,39,0,29
|
||||
2026-06-11T13:45:17,552,26,39,0,29
|
||||
2026-06-11T13:46:17,552,26,39,0,29
|
||||
2026-06-11T13:47:18,560,26,40,0,29
|
||||
2026-06-11T13:48:18,542,26,40,0,29
|
||||
2026-06-11T13:49:19,542,26,42,0,29
|
||||
2026-06-11T13:50:19,517,26,39,0,29
|
||||
2026-06-11T13:51:20,560,26,40,0,29
|
||||
2026-06-11T13:52:20,548,26,40,0,29
|
||||
2026-06-11T13:53:21,548,26,41,0,29
|
||||
2026-06-11T13:54:21,531,26,40,0,29
|
||||
2026-06-11T13:55:22,542,25.8,38,0,29
|
||||
2026-06-11T13:56:22,633,25.8,39,0,29
|
||||
2026-06-11T13:57:23,623,25.8,39,0,29
|
||||
2026-06-11T13:58:23,623,26,39,0,29
|
||||
2026-06-11T13:59:24,566,26,40,0,29
|
||||
2026-06-11T14:00:24,548,26,40,0,29
|
||||
2026-06-11T14:01:25,548,26,40,0,29
|
||||
2026-06-11T14:02:25,548,26,40,0,29
|
||||
2026-06-11T14:03:26,548,26,39,0,29
|
||||
2026-06-11T14:04:26,572,26,41,0,29
|
||||
2026-06-11T14:05:27,531,26,39,0,29
|
||||
2026-06-11T14:06:27,566,26,41,0,29
|
||||
2026-06-11T14:07:28,537,26,40,0,29
|
||||
2026-06-11T14:08:28,542,26,41,0,29
|
||||
2026-06-11T14:09:29,537,26,41,0,29
|
||||
2026-06-11T14:10:29,531,26,39,0,29
|
||||
2026-06-11T14:11:30,560,26.1,40,0,29
|
||||
2026-06-11T14:12:30,542,26.1,40,0,29
|
||||
2026-06-11T14:13:31,548,26.1,40,0,29
|
||||
2026-06-11T14:14:31,542,26.1,39,0,29
|
||||
2026-06-11T14:15:32,566,26.1,41,0,29
|
||||
2026-06-11T14:16:32,526,26.1,40,0,29
|
||||
2026-06-11T14:17:33,548,26.1,40,0,29
|
||||
2026-06-11T14:18:33,548,26.1,41,0,29
|
||||
2026-06-11T14:19:34,537,26.1,40,0,29
|
||||
2026-06-11T14:20:34,548,26.1,40,0,29
|
||||
2026-06-11T14:21:35,542,26.1,40,0,29
|
||||
2026-06-11T14:22:35,542,26.1,40,0,29
|
||||
2026-06-11T14:23:36,641,26.1,40,0,29
|
||||
2026-06-11T14:24:36,780,26.1,39,0,29
|
||||
2026-06-11T14:25:37,752,26.1,40,0,29
|
||||
2026-06-11T14:26:37,749,26.1,40,0,29
|
||||
2026-06-11T14:27:38,759,26.1,39,0,29
|
||||
2026-06-11T14:28:38,746,26.1,40,0,29
|
||||
2026-06-11T14:29:39,719,26.1,40,0,29
|
||||
2026-06-11T14:30:39,719,26.1,39,0,29
|
||||
2026-06-11T14:31:40,737,26.1,40,0,29
|
||||
2026-06-11T14:32:40,704,26.1,41,0,29
|
||||
2026-06-11T14:33:41,668,26.1,39,0,29
|
||||
2026-06-11T14:34:41,712,26.1,40,0,29
|
||||
2026-06-11T14:35:42,690,26.1,40,0,29
|
||||
2026-06-11T14:36:42,690,26.1,40,0,29
|
||||
2026-06-11T14:37:43,690,26.2,40,0,29
|
||||
2026-06-11T14:38:43,682,26.1,40,0,29
|
||||
2026-06-11T14:40:44,690,26.1,40,0,29
|
||||
2026-06-11T14:41:45,682,26.1,41,0,29
|
||||
2026-06-11T14:42:45,652,26.1,40,0,29
|
||||
2026-06-11T14:43:46,674,26.2,39,0,29
|
||||
2026-06-11T14:44:46,686,26.1,39,0,29
|
||||
2026-06-11T14:45:47,678,26.2,39,0,29
|
||||
2026-06-11T14:46:47,678,26.2,39,0,29
|
||||
2026-06-11T14:49:49,678,26.2,40,0,29
|
||||
2026-06-11T14:50:50,641,26.2,41,0,29
|
||||
2026-06-11T14:51:50,629,26.2,40,0,29
|
||||
2026-06-11T14:52:51,641,26.1,40,0,29
|
||||
2026-06-11T14:53:51,649,26.2,40,0,29
|
||||
2026-06-11T14:54:51,641,26.2,40,0,29
|
||||
2026-06-11T14:55:52,641,26.2,40,0,29
|
||||
2026-06-11T14:56:52,633,26.2,40,0,29
|
||||
2026-06-11T14:57:53,626,26.2,39,0,29
|
||||
2026-06-11T14:58:53,652,26.2,39,0,29
|
||||
2026-06-11T14:59:54,627,26.2,39,0,29
|
||||
2026-06-11T15:00:54,645,26.2,40,0,29
|
||||
2026-06-11T15:01:55,633,26.3,40,0,29
|
||||
2026-06-11T15:02:55,600,26.2,39,0,29
|
||||
2026-06-11T15:03:56,638,26.3,39,0,29
|
||||
2026-06-11T15:04:56,638,26.3,40,0,29
|
||||
2026-06-11T15:05:57,626,26.3,40,0,29
|
||||
2026-06-11T15:06:57,626,26.5,39,0,29
|
||||
2026-06-11T15:07:58,652,26.5,41,0,29
|
||||
2026-06-11T15:08:58,581,26.5,39,0,29
|
||||
2026-06-11T15:09:59,645,26.5,40,0,27
|
||||
2026-06-11T15:11:00,626,26.6,40,0,27
|
||||
2026-06-11T15:12:00,641,26.6,40,0,29
|
||||
2026-06-11T15:14:01,637,26.6,40,0,29
|
||||
2026-06-11T15:15:02,656,26.7,40,0,27
|
||||
2026-06-11T15:16:02,665,26.7,41,0,29
|
||||
2026-06-11T15:17:02,637,26.7,40,0,29
|
||||
2026-06-11T15:18:03,665,26.7,40,0,29
|
||||
2026-06-11T15:19:03,665,26.6,40,0,29
|
||||
2026-06-11T15:20:04,674,26.7,40,0,29
|
||||
2026-06-11T15:21:04,665,26.7,40,0,29
|
||||
2026-06-11T15:22:05,674,26.7,40,0,29
|
||||
2026-06-11T15:23:06,682,26.7,41,0,29
|
||||
2026-06-11T15:24:06,652,26.8,40,0,29
|
||||
2026-06-11T15:25:06,682,26.8,41,0,29
|
||||
2026-06-11T15:26:07,652,26.8,40,0,29
|
||||
2026-06-11T15:27:07,682,26.8,40,0,29
|
||||
2026-06-11T15:28:08,682,26.8,40,0,29
|
||||
2026-06-11T15:29:09,682,26.8,40,0,29
|
||||
2026-06-11T15:30:09,682,26.7,40,0,29
|
||||
2026-06-11T15:31:10,682,26.7,40,0,29
|
||||
2026-06-11T15:32:10,682,26.7,41,0,27
|
||||
2026-06-11T15:33:11,659,26.7,40,0,29
|
||||
2026-06-11T15:34:11,682,26.7,41,0,27
|
||||
2026-06-11T15:36:12,659,26.8,40,0,29
|
||||
2026-06-11T15:37:13,690,26.8,40,0,29
|
||||
2026-06-11T15:38:13,674,26.8,40,0,29
|
||||
2026-06-11T15:39:14,690,26.7,41,0,27
|
||||
2026-06-11T15:40:14,659,26.8,40,0,29
|
||||
2026-06-11T15:42:15,682,26.8,40,0,29
|
||||
2026-06-11T15:43:16,682,26.7,40,0,29
|
||||
2026-06-11T15:44:16,674,26.8,40,0,27
|
||||
2026-06-11T15:45:17,682,26.8,40,0,29
|
||||
2026-06-11T15:46:17,674,26.8,40,0,29
|
||||
2026-06-11T15:47:18,665,26.7,40,0,29
|
||||
2026-06-11T15:48:18,665,26.7,40,0,29
|
||||
2026-06-11T15:49:19,665,26.7,40,0,27
|
||||
2026-06-11T15:50:19,665,26.7,40,0,29
|
||||
2026-06-11T15:51:20,656,26.8,40,0,29
|
||||
2026-06-11T15:52:20,656,26.7,40,0,29
|
||||
2026-06-11T15:53:21,649,26.7,40,0,29
|
||||
2026-06-11T15:54:21,656,26.8,40,0,29
|
||||
2026-06-11T15:55:22,665,26.8,41,0,29
|
||||
2026-06-11T15:56:22,643,26.8,40,0,27
|
||||
2026-06-11T15:57:23,665,26.8,40,0,29
|
||||
2026-06-11T15:58:23,665,26.8,40,0,29
|
||||
2026-06-11T15:59:24,665,26.8,40,0,29
|
||||
2026-06-11T16:00:24,674,26.8,40,0,27
|
||||
2026-06-11T16:01:25,674,26.8,40,0,29
|
||||
2026-06-11T16:02:25,674,26.8,40,0,29
|
||||
2026-06-11T16:03:26,674,26.8,40,0,29
|
||||
2026-06-11T16:04:26,665,26.8,40,0,29
|
||||
2026-06-11T16:05:27,665,26.8,40,0,29
|
||||
2026-06-11T16:06:27,674,26.8,40,0,25
|
||||
2026-06-11T16:07:28,674,26.8,40,0,29
|
||||
2026-06-11T16:08:28,674,26.8,39,0,27
|
||||
2026-06-11T16:09:29,697,26.8,40,0,29
|
||||
2026-06-11T16:10:29,665,26.8,39,0,29
|
||||
2026-06-11T16:11:30,697,26.8,40,0,29
|
||||
2026-06-11T16:12:30,674,26.8,39,0,29
|
||||
2026-06-11T16:13:31,697,26.8,40,0,29
|
||||
2026-06-11T16:14:31,674,26.8,40,0,29
|
||||
2026-06-11T16:16:32,665,27,40,0,29
|
||||
2026-06-11T16:17:33,597,27,40,0,29
|
||||
2026-06-11T16:18:33,591,26.8,40,0,29
|
||||
2026-06-11T16:19:34,656,26.8,40,0,29
|
||||
2026-06-11T16:20:34,665,27,41,0,29
|
||||
2026-06-11T16:21:35,578,26.8,41,0,29
|
||||
2026-06-11T16:22:35,643,27,40,0,29
|
||||
2026-06-11T16:23:36,597,27,41,0,29
|
||||
2026-06-11T16:24:36,566,27,41,0,29
|
||||
2026-06-11T16:25:37,566,26.8,41,0,29
|
||||
2026-06-11T16:26:37,637,27,41,0,29
|
||||
2026-06-11T16:27:38,578,27,41,0,29
|
||||
2026-06-11T16:28:38,572,26.8,40,0,29
|
||||
2026-06-11T16:29:39,649,26.8,41,0,29
|
||||
|
@@ -1,23 +1,40 @@
|
||||
#import "/resources/measures/plot_measures.typ": *
|
||||
|
||||
#let start-date = datetime(year: 2026, month: 5, day: 27, hour: 14, minute: 0, second: 0)
|
||||
#let stop-date = datetime(year: 2026, month: 5, day: 27, hour: 17, minute: 59, second: 59)
|
||||
#let start-date = datetime(year: 2026, month: 6, day: 11, hour: 10, minute: 0, second: 0)
|
||||
#let stop-date = datetime(year: 2026, month: 6, day: 11, hour: 17, minute: 59, second: 59)
|
||||
|
||||
// #let node = "A2-C2_64_0F_68_35_3E"
|
||||
// #let node = "A2-C6_7E_0A_DE_DA_74"
|
||||
// #let node = "A2-E1_C0_30_15_4E_89"
|
||||
// #let node = "A2-E8_F3_0A_F7_3B_F3"
|
||||
// #let node = "A2-C6_95_1B_A6_49_E6" // Windows
|
||||
#let node = "A2-F5_80_05_76_53_F0" // Windows
|
||||
#set page(height: auto)
|
||||
|
||||
= Indoor Air Quality Analysis
|
||||
Data range: #start-date.display("[day] [month repr:long] [year] [hour]h[minute]") to #stop-date.display("[day] [month repr:long] [year] [hour]h[minute]")
|
||||
|
||||
|
||||
#let node = "A3-DC_06_D9_40_7A_CB"
|
||||
== CO2 Level #node
|
||||
|
||||
#plot_co2("/resources/measures/" + node + ".csv", start-date, stop-date)
|
||||
|
||||
#let node = "A3-E6_8A_79_C8_87_25"
|
||||
== CO2 Level #node
|
||||
|
||||
#plot_co2("/resources/measures/" + node + ".csv", start-date, stop-date)
|
||||
|
||||
#let node = "A3-ED_B2_F3_74_3E_C2"
|
||||
== CO2 Level #node
|
||||
|
||||
#plot_co2("/resources/measures/" + node + ".csv", start-date, stop-date)
|
||||
|
||||
#pagebreak()
|
||||
#let node = "A3-DC_06_D9_40_7A_CB"
|
||||
== Temperature / Humidity #node
|
||||
|
||||
#plot_temp_hum("/resources/measures/" + node + ".csv", start-date, stop-date)
|
||||
|
||||
#let node = "A3-E6_8A_79_C8_87_25"
|
||||
== Temperature / Humidity #node
|
||||
|
||||
#plot_temp_hum("/resources/measures/" + node + ".csv", start-date, stop-date)
|
||||
|
||||
#let node = "A3-ED_B2_F3_74_3E_C2"
|
||||
== Temperature / Humidity #node
|
||||
|
||||
#plot_temp_hum("/resources/measures/" + node + ".csv", start-date, stop-date)
|
||||
|
||||
@@ -128,6 +128,11 @@
|
||||
key: "svg",
|
||||
short: "SVG",
|
||||
long: "Scalable Vector Graphics"
|
||||
),
|
||||
(
|
||||
key: "trl",
|
||||
short: "TRL",
|
||||
long: "Technical Readiness Level"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||