Merge branch 'feat/test-env'
add test environment See merge request team-raclette/project-softweng!23
This commit is contained in:
@@ -8,6 +8,4 @@ MQTT_USERNAME=
|
|||||||
MQTT_PASSWORD=
|
MQTT_PASSWORD=
|
||||||
REST_USERNAME=
|
REST_USERNAME=
|
||||||
REST_PASSWORD=
|
REST_PASSWORD=
|
||||||
REST_URL=
|
|
||||||
REST_PAGE=
|
|
||||||
CYPRESS_TEST_URL=
|
CYPRESS_TEST_URL=
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
stages:
|
stages:
|
||||||
- gateway-build
|
- gateway-build
|
||||||
|
|
||||||
- web-app-build
|
- web-app-build
|
||||||
- web-app-tests
|
- web-app-tests
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -79,14 +79,23 @@ Credentials are available [on request](mailto:remi@heredero.ch).
|
|||||||
Details in [pico-sensor/README.md](pico-sensor/README.md).
|
Details in [pico-sensor/README.md](pico-sensor/README.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Test environment
|
||||||
|
[(Back to top)](#table-of-contents)
|
||||||
|
|
||||||
|
A partial test environment is deployed on the server. <br>
|
||||||
|
Web app has a second container with the same code but made a GET request on `/test` instead of `/raclette` for the
|
||||||
|
development environment. The gateway include a route to `/test` that return the 100 last measurement instead of the
|
||||||
|
last 24h. Has everything else is still a development environment, the database is the same for the test and
|
||||||
|
development environment.
|
||||||
|
|
||||||
|
|
||||||
# Authors
|
# Authors
|
||||||
|
|
||||||
[(Back to top)](#table-of-contents)
|
[(Back to top)](#table-of-contents)
|
||||||
|
|
||||||
* [Sylvan Arnold](https://github.com/Sylvan22)
|
* [Sylvan Arnold](https://github.com/Sylvan22) - Mainly pico-sensor part
|
||||||
* [Rémi Heredero](https://github.com/Klagarge)
|
* [Rémi Heredero](https://github.com/Klagarge) - Mainly gateway and docker part
|
||||||
* [Yann Sierro](https://github.com/Fastium)
|
* [Yann Sierro](https://github.com/Fastium) - Mainly web-app part
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ services:
|
|||||||
image: registry.forge.hefr.ch/team-raclette/project-softweng/gateway:latest
|
image: registry.forge.hefr.ch/team-raclette/project-softweng/gateway:latest
|
||||||
container_name: gateway
|
container_name: gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
environment:
|
environment:
|
||||||
- INFLUXDB_TOKEN=$INFLUXDB_TOKEN
|
- INFLUXDB_TOKEN=$INFLUXDB_TOKEN
|
||||||
- INFLUXDB_ORG=$INFLUXDB_ORG
|
- INFLUXDB_ORG=$INFLUXDB_ORG
|
||||||
@@ -105,15 +103,11 @@ services:
|
|||||||
image: registry.forge.hefr.ch/team-raclette/project-softweng/web-app:latest
|
image: registry.forge.hefr.ch/team-raclette/project-softweng/web-app:latest
|
||||||
container_name: web-app
|
container_name: web-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
- VUE_APP_REST_USER=$REST_USERNAME
|
- VUE_APP_REST_USER=$REST_USERNAME
|
||||||
- VUE_APP_REST_PASSWORD=$REST_PASSWORD
|
- VUE_APP_REST_PASSWORD=$REST_PASSWORD
|
||||||
- VUE_APP_REST_URL=$REST_URL
|
- VUE_APP_REST_URL=rest.mse.kb28.ch
|
||||||
- VUE_APP_REST_PAGE=$REST_PAGE
|
- VUE_APP_REST_PAGE=raclette
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.web-app-http.entrypoints=http"
|
- "traefik.http.routers.web-app-http.entrypoints=http"
|
||||||
@@ -125,3 +119,24 @@ services:
|
|||||||
- "traefik.http.services.web-app-https.loadbalancer.server.port=8080"
|
- "traefik.http.services.web-app-https.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
- "com.centurylinklabs.watchtower.enable=true"
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
|
|
||||||
|
web-app-test:
|
||||||
|
image: registry.forge.hefr.ch/team-raclette/project-softweng/web-app:latest
|
||||||
|
container_name: web-app-test
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- VUE_APP_REST_USER=$REST_USERNAME
|
||||||
|
- VUE_APP_REST_PASSWORD=$REST_PASSWORD
|
||||||
|
- VUE_APP_REST_URL=rest.mse.kb28.ch
|
||||||
|
- VUE_APP_REST_PAGE=test
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.web-app-test-http.entrypoints=http"
|
||||||
|
- "traefik.http.routers.web-app-test-http.rule=Host(`app-test.mse.kb28.ch`)"
|
||||||
|
- "traefik.http.middlewares.web-app-test-redirect.redirectscheme.scheme=https"
|
||||||
|
- "traefik.http.routers.web-app-test-https.entrypoints=https"
|
||||||
|
- "traefik.http.routers.web-app-test-https.rule=Host(`app-test.mse.kb28.ch`)"
|
||||||
|
- "traefik.http.routers.web-app-test-https.tls.certResolver=letsencrypt"
|
||||||
|
- "traefik.http.services.web-app-test-https.loadbalancer.server.port=8080"
|
||||||
|
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Gateway struct {
|
|||||||
mqtt paho.Client
|
mqtt paho.Client
|
||||||
rest *gin.Engine
|
rest *gin.Engine
|
||||||
restGroup *gin.RouterGroup
|
restGroup *gin.RouterGroup
|
||||||
|
testGroup *gin.RouterGroup
|
||||||
accounts gin.Accounts
|
accounts gin.Accounts
|
||||||
influx influxdb2.Client
|
influx influxdb2.Client
|
||||||
influxApi api.WriteAPIBlocking
|
influxApi api.WriteAPIBlocking
|
||||||
@@ -100,6 +101,7 @@ func (gh *Gateway) createRestGateway() {
|
|||||||
|
|
||||||
// Create a new router group with basic authentication for /raclette
|
// Create a new router group with basic authentication for /raclette
|
||||||
gh.restGroup = gh.rest.Group("/raclette", gin.BasicAuth(gh.accounts))
|
gh.restGroup = gh.rest.Group("/raclette", gin.BasicAuth(gh.accounts))
|
||||||
|
gh.testGroup = gh.rest.Group("/test", gin.BasicAuth(gh.accounts))
|
||||||
|
|
||||||
// Define the route for the publish command
|
// Define the route for the publish command
|
||||||
gh.restGroup.POST("", func(c *gin.Context) {
|
gh.restGroup.POST("", func(c *gin.Context) {
|
||||||
@@ -129,6 +131,14 @@ func (gh *Gateway) createRestGateway() {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": ret.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ret.Error()})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Define the route for the publish command (GET)
|
||||||
|
gh.testGroup.GET("", func(c *gin.Context) {
|
||||||
|
ret := gh.requestInfluxTest(c)
|
||||||
|
if ret != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": ret.Error()})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// createMQTTGateway initializes the MQTT client and sets up subscriptions.
|
// createMQTTGateway initializes the MQTT client and sets up subscriptions.
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ func (gh *Gateway) requestInflux(c *gin.Context) error {
|
|||||||
|> filter(fn: (r) => r["user"] == %q)
|
|> filter(fn: (r) => r["user"] == %q)
|
||||||
|> filter(fn: (r) => r["room"] == %q)
|
|> filter(fn: (r) => r["room"] == %q)
|
||||||
|> filter(fn: (r) => r["device"] == %q)
|
|> filter(fn: (r) => r["device"] == %q)
|
||||||
|
|> aggregateWindow(every: 5m, fn: mean, createEmpty: false)
|
||||||
|> sort(columns: ["_time"], desc: true)
|
|> sort(columns: ["_time"], desc: true)
|
||||||
`, INFLUXDB_BUCKET, MEASUREMENT_NAME, user, room, device)
|
`, INFLUXDB_BUCKET, MEASUREMENT_NAME, user, room, device)
|
||||||
results, err := queryAPI.Query(context.Background(), query)
|
results, err := queryAPI.Query(context.Background(), query)
|
||||||
@@ -101,3 +102,83 @@ func (gh *Gateway) requestInflux(c *gin.Context) error {
|
|||||||
c.JSON(http.StatusOK, values)
|
c.JSON(http.StatusOK, values)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gh *Gateway) requestInfluxTest(c *gin.Context) error {
|
||||||
|
// Get the user from the authenticated context
|
||||||
|
//userId := c.MustGet(gin.AuthUserKey).(string)
|
||||||
|
|
||||||
|
// Get room and device from the query parameters
|
||||||
|
user, ret := c.GetQuery("user")
|
||||||
|
if !ret {
|
||||||
|
return errors.New("no user found")
|
||||||
|
}
|
||||||
|
room, ret := c.GetQuery("room")
|
||||||
|
if !ret {
|
||||||
|
return errors.New(`no room found`)
|
||||||
|
}
|
||||||
|
device, ret := c.GetQuery("device")
|
||||||
|
if !ret {
|
||||||
|
return errors.New(`no device found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get env variables and set default values if not set
|
||||||
|
INFLUXDB_ORG, ok := os.LookupEnv("INFLUXDB_ORG")
|
||||||
|
if !ok {
|
||||||
|
log.Error("INFLUXDB_ORG not set, using default value: raclette")
|
||||||
|
INFLUXDB_ORG = "raclette"
|
||||||
|
}
|
||||||
|
|
||||||
|
INFLUXDB_BUCKET, ok := os.LookupEnv("INFLUXDB_BUCKET")
|
||||||
|
if !ok {
|
||||||
|
log.Error("INFLUXDB_BUCKET not set, using default value: raclette")
|
||||||
|
INFLUXDB_BUCKET = "raclette"
|
||||||
|
}
|
||||||
|
|
||||||
|
queryAPI := gh.influx.QueryAPI(INFLUXDB_ORG)
|
||||||
|
MEASUREMENT_NAME, ok := os.LookupEnv("MEASUREMENT_NAME")
|
||||||
|
if !ok {
|
||||||
|
log.Error("MEASUREMENT_NAME not set, using default value: softweng")
|
||||||
|
MEASUREMENT_NAME = "THC"
|
||||||
|
}
|
||||||
|
// The Flux query uses a large range (-1000d) and aggregates the latest values.
|
||||||
|
// This ensures we always get the most recent data, even if the database contains old entries.
|
||||||
|
query := fmt.Sprintf(`from(bucket: %q)
|
||||||
|
|> range(start: -1000d)
|
||||||
|
|> filter(fn: (r) => r["_measurement"] == %q)
|
||||||
|
|> filter(fn: (r) => r["user"] == %q)
|
||||||
|
|> filter(fn: (r) => r["room"] == %q)
|
||||||
|
|> filter(fn: (r) => r["device"] == %q)
|
||||||
|
|> sort(columns: ["_time"], desc: true)
|
||||||
|
|> limit(n: 100)
|
||||||
|
`, INFLUXDB_BUCKET, MEASUREMENT_NAME, user, room, device)
|
||||||
|
results, err := queryAPI.Query(context.Background(), query)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type measurement = map[string]interface{}
|
||||||
|
var values []measurement
|
||||||
|
|
||||||
|
for results.Next() {
|
||||||
|
m := measurement{}
|
||||||
|
record := results.Record()
|
||||||
|
field := record.Field()
|
||||||
|
value := record.Value()
|
||||||
|
m["time"] = record.Time()
|
||||||
|
m["value"] = value.(float64)
|
||||||
|
if field == "temperature" {
|
||||||
|
m["type"] = "temperature"
|
||||||
|
}
|
||||||
|
if field == "humidity" {
|
||||||
|
m["type"] = "humidity"
|
||||||
|
}
|
||||||
|
values = append(values, m)
|
||||||
|
fmt.Println(results.Record())
|
||||||
|
}
|
||||||
|
if err := results.Err(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, values)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ RUN \
|
|||||||
ENV PICO_SDK_PATH=/apps/pico-sdk/
|
ENV PICO_SDK_PATH=/apps/pico-sdk/
|
||||||
|
|
||||||
# Patch the SDK for semihosting file I/O (needed for gcov)
|
# Patch the SDK for semihosting file I/O (needed for gcov)
|
||||||
COPY newlib_interface.c.patched ${PICO_SDK_PATH}/src/rp2_common/pico_clib_interface/newlib_interface.c
|
COPY ./pico-sensor/newlib_interface.c.patched ${PICO_SDK_PATH}/src/rp2_common/pico_clib_interface/newlib_interface.c
|
||||||
|
|
||||||
# Build picotool so we don't have to build it for the project
|
# Build picotool so we don't have to build it for the project
|
||||||
RUN \
|
RUN \
|
||||||
Reference in New Issue
Block a user