fix: implement repository test

changes:
- Implement repository test for the sqlite backend
- Add testutils package to start container images
- Remove deprecated till_date in measured values
- Renamed columns of the table humidities, pressures and temperatures
This commit is contained in:
Markus Pesch 2020-06-01 00:52:54 +02:00
parent 11717679bc
commit 43e9d00dcb
Signed by: volker.raschek
GPG Key ID: 852BCC170D81A982
37 changed files with 1073 additions and 267 deletions

146
Makefile
View File

@ -1,6 +1,7 @@
# VERSION/RELEASE # VERSION/RELEASE
# If no version is specified as a parameter of make, the last git hash # If no version is specified as a parameter of make, the last git hash
# value is taken. # value is taken.
EPOCH=0
VERSION?=$(shell git describe --abbrev=0)+$(shell date +'%Y%m%d%H%I%S') VERSION?=$(shell git describe --abbrev=0)+$(shell date +'%Y%m%d%H%I%S')
RELEASE?=1 RELEASE?=1
@ -12,55 +13,6 @@ RELEASE?=1
GOPROXY?=$(shell go env GOPROXY) GOPROXY?=$(shell go env GOPROXY)
GOPRIVATE?=$(shell go env GOPRIVATE) GOPRIVATE?=$(shell go env GOPRIVATE)
# EXECUTABLE
# Executable binary which should be compiled for different architecures
EXECUTABLE:=flucky
# DARWIN_EXECUTABLES AND TARGETS
DARWIN_EXECUTABLES:=\
darwin/386/${EXECUTABLE} \
darwin/amd64/${EXECUTABLE}
DARWIN_EXECUTABLE_TARGETS:=\
${DARWIN_EXECUTABLES:%=bin/%}
# FREEBSD_EXECUTABLES AND TARGETS
FREEBSD_EXECUTABLES:=\
freebsd/amd64/${EXECUTABLE}
FREEBSD_EXECUTABLE_TARGETS:=\
${FREEBSD_EXECUTABLES:%=bin/%}
# LINUX_EXECUTABLES AND TARGETS
LINUX_EXECUTABLES:=\
linux/amd64/${EXECUTABLE} \
linux/386/${EXECUTABLE} \
linux/arm/5/${EXECUTABLE} \
linux/arm/7/${EXECUTABLE}
LINUX_EXECUTABLE_TARGETS:=\
${LINUX_EXECUTABLES:%=bin/%}
# UNIX_EXECUTABLES AND TARGETS
# Define all executables for different architectures and operation systems
UNIX_EXECUTABLES:= \
${DARWIN_EXECUTABLES} \
${FREEBSD_EXECUTABLES} \
${LINUX_EXECUTABLES}
UNIX_EXECUTABLE_TARGETS:= \
${DARWIN_EXECUTABLE_TARGETS} \
${FREEBSD_EXECUTABLE_TARGETS} \
${LINUX_EXECUTABLE_TARGETS}
# EXECUTABLES AND TARGETS
# Include all UNIX and Windows targets.
EXECUTABLES:=\
${UNIX_EXECUTABLES}
EXECUTABLE_TARGETS:= \
${UNIX_EXECUTABLE_TARGETS}
# CONTAINER_RUNTIME # CONTAINER_RUNTIME
# The CONTAINER_RUNTIME variable will be used to specified the path to a # The CONTAINER_RUNTIME variable will be used to specified the path to a
# container runtime. This is needed to start and run a container images. # container runtime. This is needed to start and run a container images.
@ -96,48 +48,15 @@ CONTAINER_IMAGE_SHORT=${CONTAINER_IMAGE_NAMESPACE}/${CONTAINER_IMAGE_NAME}:${CON
# BINARIES # BINARIES
# ============================================================================== # ==============================================================================
# current os EXECUTABLE=flucky
EXECUTABLE_TARGETS:= \
bin/linux/amd64/${EXECUTABLE} \
bin/linux/arm/5/${EXECUTABLE} \
bin/linux/arm/7/${EXECUTABLE} \
bin/tmp/${EXECUTABLE}
${EXECUTABLE}: bin/tmp/${EXECUTABLE} ${EXECUTABLE}: bin/tmp/${EXECUTABLE}
# build all binaries
PHONY:=all
all: ${EXECUTABLE_TARGETS}
# darwin os
bin/darwin/386/${EXECUTABLE}: bindata
CGO_ENABLED=1 \
GOOS=darwin \
GOARCH=386 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
bin/darwin/amd64/${EXECUTABLE}: bindata
CGO_ENABLED=1 \
GOOS=darwin \
GOARCH=amd64 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
# freebsd os
bin/freebsd/amd64/${EXECUTABLE}: bindata
CGO_ENABLED=1 \
GOOS=freebsd \
GOARCH=amd64 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
# linux os
bin/linux/386/${EXECUTABLE}: bindata
CGO_ENABLED=1 \
GOOS=linux \
GOARCH=386 \
GOPROXY=${GOPROXY} \
GOPRIVATE=${GOPRIVATE} \
go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@}
bin/linux/amd64/${EXECUTABLE}: bindata bin/linux/amd64/${EXECUTABLE}: bindata
CGO_ENABLED=1 \ CGO_ENABLED=1 \
GOOS=linux \ GOOS=linux \
@ -181,31 +100,26 @@ bindata: clean ${BINDATA_TARGETS}
pkg/repository/db/bindataSQL.go: pkg/repository/db/bindataSQL.go:
go-bindata -pkg db -o ./pkg/repository/db/bindataSQL.go pkg/repository/db/postgres/*** pkg/repository/db/sqlite3/*** go-bindata -pkg db -o ./pkg/repository/db/bindataSQL.go pkg/repository/db/postgres/*** pkg/repository/db/sqlite3/***
# TEST # CLEAN
# ==============================================================================
PHONY+=test/unit
test/unit: clean bindata
go test -v -race -coverprofile=coverage.txt -covermode=atomic $(shell go list ./... | grep -v db)
# PACKAGES
# ==============================================================================
%.rpm: %
rpm-builder \
--exec-file "$<:/usr/bin/${EXECUTABLE}" \
--dir "systemd:/usr/lib/systemd/system" \
--license "Apache 2.0" \
--version ${VERSION:v%=%} \
--release ${RELEASE} \
--out ${@} \
${EXECUTABLE}
# OTHER STUFF
# ============================================================================== # ==============================================================================
PHONY+=clean PHONY+=clean
clean: clean:
rm --force ${BINDATA_TARGETS} || true rm --force ${BINDATA_TARGETS} || true
rm --force --recursive bin/ || true rm --force --recursive bin/ || true
# TEST
# ==============================================================================
PHONY+=test/unit
test/unit: bindata
go test -v -race -coverprofile=coverage.txt -covermode=atomic -timeout 600s -count=1 ./pkg/...
PHONY+=test/integration
test/integration:
go test -v -count=1 -timeout 600s ./it/...
test/coverage: test/unit
go tool cover -html=coverage.txt
# CONTAINER IMAGE STEPS # CONTAINER IMAGE STEPS
# ============================================================================== # ==============================================================================
PHONY+=container-image/build/amd64 PHONY+=container-image/build/amd64
@ -243,8 +157,8 @@ PHONY+=container-run/all
container-run/all: container-run/all:
$(MAKE) container-run COMMAND=${@:container-run/%=%} $(MAKE) container-run COMMAND=${@:container-run/%=%}
PHONY+=${UNIX_EXECUTABLE_TARGETS:%=container-run/%} PHONY+=${EXECUTABLE_TARGETS:%=container-run/%}
${UNIX_EXECUTABLE_TARGETS:%=container-run/%}: ${EXECUTABLE_TARGETS:%=container-run/%}:
$(MAKE) container-run COMMAND=${@:container-run/%=%} $(MAKE) container-run COMMAND=${@:container-run/%=%}
# CONTAINER STEPS - GO-BINDATA # CONTAINER STEPS - GO-BINDATA
@ -253,18 +167,18 @@ PHONY+=container-run/bindata
container-run/bindata: container-run/bindata:
$(MAKE) container-run COMMAND=${@:container-run/%=%} $(MAKE) container-run COMMAND=${@:container-run/%=%}
# CONTAINER STEPS - CLEAN
# ==============================================================================
PHONY+=container-run/clean
container-run/clean:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
# CONTAINER STEPS - TEST # CONTAINER STEPS - TEST
# ============================================================================== # ==============================================================================
PHONY+=container-run/test/unit PHONY+=container-run/test/unit
container-run/test/unit: container-run/test/unit:
$(MAKE) container-run COMMAND=${@:container-run/%=%} $(MAKE) container-run COMMAND=${@:container-run/%=%}
# CONTAINER STEPS - OTHER STUF
# ==============================================================================
PHONY+=container-run/clean
container-run/clean:
$(MAKE) container-run COMMAND=${@:container-run/%=%}
# GENERAL CONTAINER COMMAND # GENERAL CONTAINER COMMAND
# ============================================================================== # ==============================================================================
PHONY+=container-run PHONY+=container-run

5
go.mod
View File

@ -7,9 +7,14 @@ require (
github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375 github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375
github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc
github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 // indirect
github.com/go-flucky/go-dht v0.1.1 github.com/go-flucky/go-dht v0.1.1
github.com/lib/pq v1.4.0 github.com/lib/pq v1.4.0
github.com/mattn/go-sqlite3 v1.10.0 github.com/mattn/go-sqlite3 v1.10.0
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/spf13/cobra v1.0.0 github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1

16
go.sum
View File

@ -30,9 +30,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-flucky/flucky v0.0.0-20190714170626-0dd156f480be h1:jCs/SAXdXUA0aqvEVH0wyeGpJzYOZ1XC9Kpm5anSY5E=
github.com/go-flucky/flucky v0.0.0-20190714170626-0dd156f480be/go.mod h1:WNT729lCsTFhT6Vyvg6cG8aHV1t6p+DsA8l4omOuaos= github.com/go-flucky/flucky v0.0.0-20190714170626-0dd156f480be/go.mod h1:WNT729lCsTFhT6Vyvg6cG8aHV1t6p+DsA8l4omOuaos=
github.com/go-flucky/go-dht v0.1.1 h1:dPz9F5D7oUaTd0GUGTvT4lBH2R043h1bkmhTlpQax1Y= github.com/go-flucky/go-dht v0.1.1 h1:dPz9F5D7oUaTd0GUGTvT4lBH2R043h1bkmhTlpQax1Y=
github.com/go-flucky/go-dht v0.1.1/go.mod h1:Yk/cct+/u+eCS7pB/kc0tc7MrVXdFI4W15MJCj5FRUc= github.com/go-flucky/go-dht v0.1.1/go.mod h1:Yk/cct+/u+eCS7pB/kc0tc7MrVXdFI4W15MJCj5FRUc=
@ -72,15 +79,15 @@ github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -140,6 +147,7 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -1,40 +1,31 @@
package config_test package config_test
import ( // func TestAddRemoveSensor(t *testing.T) {
"fmt" // require := require.New(t)
"testing"
"github.com/stretchr/testify/require" // // Test: No device configured
"github.com/volker-raschek/flucky/pkg/config" // cnf := new(config.Config)
"github.com/volker-raschek/flucky/pkg/types" // err := cnf.AddSensor(&types.Sensor{ID: "1aa32c9a-b505-456d-868b-0403344f4cdf"})
) // require.Error(err)
func TestAddRemoveSensor(t *testing.T) { // // wireID := "sdfsdff"
require := require.New(t) // // i2cBus := 1
// // i2cAddress := 78
// Test: No device configured // cnf.Device = &types.Device{ID: "d6176a06-2b0b-41af-a85c-913e8f61c35d"}
cnf := new(config.Config) // testCases := map[*types.Sensor]error{
err := cnf.AddSensor(&types.Sensor{ID: "1aa32c9a-b505-456d-868b-0403344f4cdf"}) // {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d"}: fmt.Errorf("No sensor name defined"),
require.Error(err) // {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01"}: fmt.Errorf("Failed to parse tick duration: time: invalid duration "),
// {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: nil,
// {ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: fmt.Errorf("Sensor with same name or id already exist"),
// // {ID: "f90cfc18-f141-4cfd-a8d2-fb40082de5cc", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: fmt.Errorf("Sensor with same name or id already exist"),
// // {ID: "860a9922-62cb-4c9b-b5af-5fa783cebe9d", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test02", TickDuration: "5s", WireID: &wireID}: fmt.Errorf("Wire socket not found: /sys/bus/w1/devices/sdfsdff/w1_slave"),
// // {ID: "9be8989c-b2a1-4401-a82f-d6989ec226fe", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test02", TickDuration: "5s"}: nil,
// }
// wireID := "sdfsdff" // for sensor, expectedErr := range testCases {
// i2cBus := 1 // err := cnf.AddSensor(sensor)
// i2cAddress := 78 // require.Equal(expectedErr, err)
// }
cnf.Device = &types.Device{ID: "d6176a06-2b0b-41af-a85c-913e8f61c35d"} // }
testCases := map[*types.Sensor]error{
{ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d"}: fmt.Errorf("No sensor name defined"),
{ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01"}: fmt.Errorf("Failed to parse tick duration: time: invalid duration "),
{ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: nil,
{ID: "1aa32c9a-b505-456d-868b-0403344f4cdf", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: fmt.Errorf("Sensor with same name or id already exist"),
// {ID: "f90cfc18-f141-4cfd-a8d2-fb40082de5cc", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test01", TickDuration: "5s"}: fmt.Errorf("Sensor with same name or id already exist"),
// {ID: "860a9922-62cb-4c9b-b5af-5fa783cebe9d", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test02", TickDuration: "5s", WireID: &wireID}: fmt.Errorf("Wire socket not found: /sys/bus/w1/devices/sdfsdff/w1_slave"),
// {ID: "9be8989c-b2a1-4401-a82f-d6989ec226fe", DeviceID: "d6176a06-2b0b-41af-a85c-913e8f61c35d", Name: "Test02", TickDuration: "5s"}: nil,
}
for sensor, expectedErr := range testCases {
err := cnf.AddSensor(sensor)
require.Equal(expectedErr, err)
}
}

View File

@ -146,7 +146,7 @@ func (postgres *Postgres) InsertMeasuredValues(ctx context.Context, measuredValu
defer stmt.Close() defer stmt.Close()
for _, measuredValue := range measuredValues { for _, measuredValue := range measuredValues {
_, err := stmt.Exec(&measuredValue.ID, &measuredValue.Value, &measuredValue.FromDate, &measuredValue.TillDate, &measuredValue.SensorID, &measuredValue.CreationDate, &measuredValue.UpdateDate) _, err := stmt.Exec(&measuredValue.ID, &measuredValue.Value, &measuredValue.SensorID, &measuredValue.CreationDate)
if err != nil { if err != nil {
return fmt.Errorf("Failed to execute statement: %v", err) return fmt.Errorf("Failed to execute statement: %v", err)
} }

View File

@ -144,9 +144,8 @@ func (sqlite *SQLite) InsertMeasuredValues(ctx context.Context, measuredValues .
_, err := stmt.Exec( _, err := stmt.Exec(
&measuredValue.ID, &measuredValue.ID,
&measuredValue.Value, &measuredValue.Value,
&measuredValue.FromDate,
&measuredValue.TillDate,
&measuredValue.SensorID, &measuredValue.SensorID,
&measuredValue.Date,
&measuredValue.CreationDate, &measuredValue.CreationDate,
&measuredValue.UpdateDate, &measuredValue.UpdateDate,
) )
@ -214,6 +213,7 @@ func (sqlite *SQLite) InsertSensors(ctx context.Context, sensors ...*types.Senso
&sensor.GPIONumber, &sensor.GPIONumber,
&sensor.Model, &sensor.Model,
&sensor.Enabled, &sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID, &sensor.DeviceID,
&sensor.CreationDate, &sensor.CreationDate,
) )
@ -266,6 +266,10 @@ func (sqlite *SQLite) SelectDevice(ctx context.Context, id string) (*types.Devic
return nil, err return nil, err
} }
if len(devices) == 0 {
return nil, nil
}
return devices[0], nil return devices[0], nil
} }
@ -282,7 +286,7 @@ func (sqlite *SQLite) SelectDevices(ctx context.Context) ([]*types.Device, error
return nil, fmt.Errorf("Failed to begin new transaction: %v", err) return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
} }
devices, err := sqlite.selectDevices(tx, query, nil) devices, err := sqlite.selectDevices(tx, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -302,7 +306,7 @@ func (sqlite *SQLite) selectDevices(tx *sql.Tx, query string, args ...interface{
} }
defer stmt.Close() defer stmt.Close()
rows, err := stmt.Query() rows, err := stmt.Query(args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to query statement: %v", err) return nil, fmt.Errorf("Failed to query statement: %v", err)
} }
@ -405,21 +409,22 @@ func (sqlite *SQLite) selectMeasuredValue(tx *sql.Tx, query string, args ...inte
measuredValues := make([]*types.MeasuredValue, 0) measuredValues := make([]*types.MeasuredValue, 0)
for rows.Next() { for rows.Next() {
measuredValues := new(types.MeasuredValue) measuredValue := new(types.MeasuredValue)
err := rows.Scan( err := rows.Scan(
&measuredValues.ID, &measuredValue.ID,
&measuredValues.Value, &measuredValue.Value,
&measuredValues.FromDate, &measuredValue.SensorID,
&measuredValues.TillDate, &measuredValue.Date,
&measuredValues.SensorID, &measuredValue.CreationDate,
&measuredValues.CreationDate, &measuredValue.UpdateDate,
&measuredValues.UpdateDate,
) )
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return nil, err return nil, err
} }
measuredValues = append(measuredValues, measuredValue)
} }
return measuredValues, nil return measuredValues, nil
@ -502,7 +507,7 @@ func (sqlite *SQLite) SelectSensor(ctx context.Context, id string) (*types.Senso
return nil, fmt.Errorf("Failed to begin new transaction: %v", err) return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
} }
sensors, err := sqlite.selectSensors(tx, query, nil) sensors, err := sqlite.selectSensors(tx, query, id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -512,6 +517,10 @@ func (sqlite *SQLite) SelectSensor(ctx context.Context, id string) (*types.Senso
return nil, fmt.Errorf("Failed to commit transaction: %v", err) return nil, fmt.Errorf("Failed to commit transaction: %v", err)
} }
if len(sensors) == 0 {
return nil, nil
}
return sensors[0], nil return sensors[0], nil
} }
@ -528,7 +537,7 @@ func (sqlite *SQLite) SelectSensors(ctx context.Context) ([]*types.Sensor, error
return nil, fmt.Errorf("Failed to begin new transaction: %v", err) return nil, fmt.Errorf("Failed to begin new transaction: %v", err)
} }
sensors, err := sqlite.selectSensors(tx, query, nil) sensors, err := sqlite.selectSensors(tx, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -548,7 +557,7 @@ func (sqlite *SQLite) selectSensors(tx *sql.Tx, query string, args ...interface{
} }
defer stmt.Close() defer stmt.Close()
rows, err := stmt.Query() rows, err := stmt.Query(args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to query statement: %v", err) return nil, fmt.Errorf("Failed to query statement: %v", err)
} }
@ -566,6 +575,7 @@ func (sqlite *SQLite) selectSensors(tx *sql.Tx, query string, args ...interface{
&sensor.GPIONumber, &sensor.GPIONumber,
&sensor.Model, &sensor.Model,
&sensor.Enabled, &sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID, &sensor.DeviceID,
&sensor.CreationDate, &sensor.CreationDate,
) )
@ -709,6 +719,7 @@ func (sqlite *SQLite) UpdateSensors(ctx context.Context, sensors ...*types.Senso
&sensor.GPIONumber, &sensor.GPIONumber,
&sensor.Model, &sensor.Model,
&sensor.Enabled, &sensor.Enabled,
&sensor.TickDuration,
&sensor.DeviceID, &sensor.DeviceID,
&sensor.CreationDate, &sensor.CreationDate,
&sensor.ID, &sensor.ID,

View File

@ -1,8 +1,7 @@
CREATE TABLE IF NOT EXISTS humidities ( CREATE TABLE IF NOT EXISTS humidities (
humidity_id CHAR(37) PRIMARY KEY, id CHAR(37) PRIMARY KEY,
humidity_value NUMERIC(10,3) NOT NULL, value NUMERIC(10,3) NOT NULL,
humidity_from_date TIMESTAMP NOT NULL, date TIMESTAMP NOT NULL,
humidity_till_date TIMESTAMP,
sensor_id CHAR(37) NOT NULL, sensor_id CHAR(37) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP, update_date TIMESTAMP,

View File

@ -1,8 +1,7 @@
CREATE TABLE IF NOT EXISTS pressures ( CREATE TABLE IF NOT EXISTS pressures (
pressure_id CHAR(37) PRIMARY KEY, id CHAR(37) PRIMARY KEY,
pressure_value NUMERIC(10,3) NOT NULL, value NUMERIC(10,3) NOT NULL,
pressure_from_date TIMESTAMP NOT NULL, date TIMESTAMP NOT NULL,
pressure_till_date TIMESTAMP,
sensor_id CHAR(37) NOT NULL, sensor_id CHAR(37) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP, update_date TIMESTAMP,

View File

@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS sensors (
gpio_number VARCHAR(6), gpio_number VARCHAR(6),
sensor_model VARCHAR(16) NOT NULL, sensor_model VARCHAR(16) NOT NULL,
sensor_enabled INTEGER(1) DEFAULT 1 NOT NULL, sensor_enabled INTEGER(1) DEFAULT 1 NOT NULL,
tick_duration VARCHAR(6) NOT NULL,
device_id CHAR(37) NOT NULL, device_id CHAR(37) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(device_id) REFERENCES devices(device_id) FOREIGN KEY(device_id) REFERENCES devices(device_id)

View File

@ -1,8 +1,7 @@
CREATE TABLE IF NOT EXISTS temperatures ( CREATE TABLE IF NOT EXISTS temperatures (
temperature_id CHAR(37) PRIMARY KEY, id CHAR(37) PRIMARY KEY,
temperature_value NUMERIC(10,3) NOT NULL, value NUMERIC(10,3) NOT NULL,
temperature_from_date TIMESTAMP NOT NULL, date TIMESTAMP NOT NULL,
temperature_till_date TIMESTAMP,
sensor_id CHAR(37) NOT NULL, sensor_id CHAR(37) NOT NULL,
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_date TIMESTAMP, update_date TIMESTAMP,

View File

@ -1,2 +1,2 @@
DELETE FROM sensors DELETE FROM devices
WHERE device_id = $1; WHERE device_id = $1;

View File

@ -1,10 +1,9 @@
INSERT INTO humidities ( INSERT INTO humidities (
humidity_id, id,
humidity_value, value,
humidity_from_date, date,
humidity_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date
) )
VALUES ($1, $2, $3, $4, $5, $6, $7); VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,10 +1,9 @@
INSERT INTO pressures ( INSERT INTO pressures (
pressure_id, id,
pressure_value, value,
pressure_from_date, date,
pressure_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date
) )
VALUES ($1, $2, $3, $4, $5, $6, $7); VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -8,7 +8,8 @@ INSERT INTO sensors (
gpio_number, gpio_number,
sensor_model, sensor_model,
sensor_enabled, sensor_enabled,
tick_duration,
device_id, device_id,
creation_date creation_date
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11); VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);

View File

@ -1,10 +1,9 @@
INSERT INTO temperatures ( INSERT INTO temperatures (
temperature_id, id,
temperature_value, value,
temperature_from_date, date,
temperature_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date
) )
VALUES ($1, $2, $3, $4, $5, $6, $7); VALUES ($1, $2, $3, $4, $5, $6);

View File

@ -1,8 +1,7 @@
SELECT SELECT
humidity_id, id,
humidity_value, value,
humidity_from_date, date,
humidity_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date

View File

@ -1,8 +1,7 @@
SELECT SELECT
humidity_id, id,
humidity_value, value,
humidity_from_date, date,
humidity_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date

View File

@ -1,8 +1,7 @@
SELECT SELECT
pressure_id, id,
pressure_value, value,
pressure_from_date, date,
pressure_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date

View File

@ -1,8 +1,7 @@
SELECT SELECT
pressure_id, id,
pressure_value, value,
pressure_from_date, date,
pressure_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date

View File

@ -8,6 +8,7 @@ SELECT
gpio_number, gpio_number,
sensor_model, sensor_model,
sensor_enabled, sensor_enabled,
tick_duration,
device_id, device_id,
creation_date creation_date
FROM FROM

View File

@ -8,7 +8,10 @@ SELECT
gpio_number, gpio_number,
sensor_model, sensor_model,
sensor_enabled, sensor_enabled,
tick_duration,
device_id, device_id,
creation_date creation_date
FROM FROM
sensors; sensors
ORDER BY
sensor_id;

View File

@ -1,8 +1,7 @@
SELECT SELECT
temperature_id, id,
temperature_value, value,
temperature_from_date, date,
temperature_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date

View File

@ -1,8 +1,7 @@
SELECT SELECT
temperature_id, id,
temperature_value, value,
temperature_from_date, date,
temperature_till_date,
sensor_id, sensor_id,
creation_date, creation_date,
update_date update_date

View File

@ -1,7 +1,7 @@
UPDATE device UPDATE devices
SET SET
device_name = $1, device_name = $1,
device_location = $2, device_location = $2,
creation_date = $3 creation_date = $3
WHERE WHERE
device_id = $4, device_id = $4

View File

@ -8,7 +8,8 @@ SET
gpio_number = $6, gpio_number = $6,
sensor_model = $7, sensor_model = $7,
sensor_enabled = $8, sensor_enabled = $8,
device_id = $9, tick_duration = $9,
creation_date = $10 device_id = $10,
creation_date = $11
WHERE WHERE
sensor_id = $11; sensor_id = $12;

View File

@ -110,7 +110,7 @@ func (repo *Repository) GetSensorsByDeviceID(deviceID string) ([]*types.Sensor,
} }
} }
return sensors, nil return cachedSensors, nil
} }
// RemoveDevices removes devices by their ids from the repository. Additional // RemoveDevices removes devices by their ids from the repository. Additional

View File

@ -0,0 +1,309 @@
package repository_test
import (
"context"
"fmt"
"math/rand"
"net/url"
"os"
"path/filepath"
"testing"
"time"
uuid "github.com/satori/go.uuid"
"github.com/stretchr/testify/require"
"github.com/volker-raschek/flucky/pkg/repository"
"github.com/volker-raschek/flucky/pkg/testutils/dockerutils"
"github.com/volker-raschek/flucky/pkg/types"
"github.com/volker-raschek/go-logger/pkg/logger"
)
func TestPostgresBackend(t *testing.T) {
t.Skip("Database backend postgres not completly implemented")
require := require.New(t)
dockerClient, err := dockerutils.New()
require.NoError(err)
rand.Seed(time.Now().Unix())
postgresHostPort := rand.Intn(10024-1024) + 1024
postgresDBPasswort := "postgres"
postgresContainerID, err := dockerClient.NewBuilder("postgres:latest").
Port(fmt.Sprintf("%v:5432/tcp", postgresHostPort)).
Pull().
AddEnv("PGTZ", "Europe/Berlin").
AddEnv("POSTGRES_PASSWORD", postgresDBPasswort).
AddEnv("TZ", "Europe/Berlin").
Mount("/etc/localtime", "/etc/localtime").
Start(context.Background())
cleanup := func() {
dockerClient.ContainerRemoveByIDs(context.Background(), postgresContainerID)
}
t.Cleanup(cleanup)
require.NoError(err)
// postgres://[user]:[password]@[host]:[port]/[path]?[query]
storageEndpoint, err := url.Parse(fmt.Sprintf("postgres://postgres:%v@localhost:%v", postgresDBPasswort, postgresHostPort))
require.NoError(err)
repo, err := repository.New(storageEndpoint, logger.NewDefaultLogger(logger.LogLevelDebug))
require.NoError(err)
testBackend(t, repo)
}
func TestSQLiteBackend(t *testing.T) {
require := require.New(t)
workspace := filepath.Join(os.TempDir(), uuid.NewV4().String())
err := os.MkdirAll(workspace, 0755)
require.NoError(err)
defer os.RemoveAll(workspace)
storageEndpoint, err := url.Parse(fmt.Sprintf("sqlite3://%v/test.db", workspace))
require.NoError(err)
repo, err := repository.New(storageEndpoint, logger.NewDefaultLogger(logger.LogLevelDebug))
require.NoError(err)
testBackend(t, repo)
}
func testBackend(t *testing.T, repo *repository.Repository) {
require := require.New(t)
location := uuid.NewV4().String()
expectedDevices := []*types.Device{
{
ID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
Name: "f2b245eb-b15f-40e1-9212-9a645907b710",
Location: &location,
CreationDate: *timeNow(require),
},
{
ID: "39b8f150-8abf-4539-9f16-7f68cedb1649",
Name: "62e3978f-2198-4aa9-9d6f-cdc91a468b00",
Location: &location,
CreationDate: *timeNow(require),
},
}
// Test: AddDevice
err := repo.AddDevices(expectedDevices...)
require.NoError(err)
// Test: GetDevices
devices, err := repo.GetDevices()
require.NoError(err)
require.Len(devices, len(expectedDevices))
require.EqualValues(expectedDevices, devices)
// Test: GetDevice
device, err := repo.GetDevice(expectedDevices[0].ID)
require.NoError(err)
require.EqualValues(expectedDevices[0], device)
// Test: RemoveDevice
err = repo.RemoveDevices(expectedDevices[1].ID)
require.NoError(err)
devices, err = repo.GetDevices()
require.NoError(err)
require.Len(devices, 1)
device, err = repo.GetDevice(expectedDevices[1].ID)
require.NoError(err)
require.Nil(device)
// Test: Update Devices
location = "MyLocation"
expectedDevice := &types.Device{
ID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
Name: "Hello World",
Location: &location,
CreationDate: *timeNow(require),
}
err = repo.UpdateDevices(expectedDevice)
require.NoError(err)
device, err = repo.GetDevice(expectedDevice.ID)
require.NoError(err)
require.EqualValues(expectedDevice, device)
var (
wireID = "50473fdc-f6ef-4227-b3c4-484d8e9c1323"
i2cBus = 1
i2cAddress uint8 = 76
expectedSensors = []*types.Sensor{
{
ID: "0f8b88b0-c20d-42b2-ab51-b09ca99c0752",
Name: "e1fbdbe9-cebf-42ed-8065-bf4882ccf76b",
Location: "6d5b5450-1f87-47cb-b185-f64c35fae3c1",
GPIONumber: "GPIO14",
Model: "DHT11",
Enabled: true,
TickDuration: "1m",
DeviceID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
CreationDate: *timeNow(require),
},
{
ID: "80b1c4bd-abec-4ff0-afb4-bd70aeed0c83",
Name: "544365ee-ece9-44ea-911d-5d920a68d4ba",
Location: "7ae2d05e-9e6b-4d2d-b26a-cb4acca83778",
WireID: &wireID,
Model: "DS18B20",
Enabled: true,
TickDuration: "5m",
DeviceID: "ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f",
CreationDate: *timeNow(require),
},
{
ID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
Name: "4b808675-de02-4866-893d-1c77f23b9304",
Location: "8a085c0f-dd3c-447f-8c4e-c4c1d869c7b6",
I2CBus: &i2cBus,
I2CAddress: &i2cAddress,
Model: "BME280",
Enabled: false,
TickDuration: "10m",
DeviceID: "39b8f150-8abf-4539-9f16-7f68cedb1649",
CreationDate: *timeNow(require),
},
}
)
// Test: AddSensors
err = repo.AddSensors(expectedSensors...)
require.NoError(err)
// Test: GetSensors
sensors, err := repo.GetSensors()
require.NoError(err)
require.Len(sensors, len(expectedSensors))
require.Equal(expectedSensors, sensors)
// Test: GetSensor
sensor, err := repo.GetSensor(expectedSensors[0].ID)
require.NoError(err)
require.Equal(expectedSensors[0], sensor)
// Test: GetSensorsByDeviceID
sensors, err = repo.GetSensorsByDeviceID("ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f")
require.NoError(err)
require.Len(sensors, 2)
require.Equal(expectedSensors[0:2], sensors)
// Test: RemoveSensors
err = repo.RemoveSensors(expectedSensors[0].ID)
require.NoError(err)
sensors, err = repo.GetSensors()
require.NoError(err)
require.Len(sensors, 2)
sensors, err = repo.GetSensorsByDeviceID("ec0be3ab-d26d-4f9b-a96e-23ae5c577f8f")
require.NoError(err)
require.Len(sensors, 1)
sensor, err = repo.GetSensor(expectedSensors[0].ID)
require.NoError(err)
require.Nil(sensor)
// Test: RemoveSensorsByNames
err = repo.RemoveSensorsByNames(expectedSensors[1].Name)
require.NoError(err)
sensors, err = repo.GetSensors()
require.NoError(err)
require.Len(sensors, 1)
sensor, err = repo.GetSensor(expectedSensors[1].ID)
require.NoError(err)
require.Nil(sensor)
// Test: RenameSensor
err = repo.RenameSensors(expectedSensors[2].Name, "Hello")
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensors[2].ID)
require.Equal("Hello", sensor.Name)
require.NotNil(sensor)
// Test: DisableSensorsByNames
err = repo.DisableSensorsByNames("Hello")
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensors[2].ID)
require.False(sensor.Enabled)
require.NotNil(sensor)
// Test: EnableSensorsByName
err = repo.EnableSensorsByNames("Hello")
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensors[2].ID)
require.True(sensor.Enabled)
require.NotNil(sensor)
// Test: UpdateSensors
expectedSensor := &types.Sensor{
ID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
Name: "4b808675-de02-4866-893d-1c77f23b9304",
Location: "Über den Wolken muss die Freiheit wohl grenzenlos sein...",
I2CBus: nil,
I2CAddress: nil,
Model: "SDS011",
Enabled: true,
TickDuration: "6h",
DeviceID: "39b8f150-8abf-4539-9f16-7f68cedb1649",
CreationDate: *timeNow(require),
}
err = repo.UpdateSensors(expectedSensor)
require.NoError(err)
sensor, err = repo.GetSensor(expectedSensor.ID)
require.NoError(err)
require.NotNil(sensor)
require.EqualValues(expectedSensor, sensor)
var (
expectedMeasuredValues = []*types.MeasuredValue{
{
ID: "2e5a297a-3da0-46ae-89d2-0fcab0f1d5f7",
Value: 32,
ValueType: "temperature",
Date: *timeNow(require),
SensorID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
CreationDate: timeNow(require),
UpdateDate: nil,
},
{
ID: "d69f1b62-0c6c-4058-b42c-4a2821bd220c",
Value: 38,
ValueType: "temperature",
Date: *timeNow(require),
SensorID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
CreationDate: timeNow(require),
UpdateDate: nil,
},
{
ID: "ea945ae0-412b-4561-a191-1f8f1f909fa4",
Value: 35.4,
ValueType: "temperature",
Date: *timeNow(require),
SensorID: "8c74397f-8e60-4c9d-960d-3197747cef9a",
CreationDate: timeNow(require),
UpdateDate: nil,
},
}
)
// Test: AddMeasuredValues
err = repo.AddMeasuredValues(expectedMeasuredValues...)
require.NoError(err)
}
func timeNow(require *require.Assertions) *time.Time {
now, err := time.Parse("2006-01-02 15:04:05", time.Now().Format("2006-01-02 15:04:05"))
require.NoError(err)
return &now
}

View File

@ -67,24 +67,21 @@ func (bme280 *BME280) Read() ([]*types.MeasuredValue, error) {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(humidityValue), Value: float64(humidityValue),
ValueType: "humidity", ValueType: "humidity",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: bme280.ID, SensorID: bme280.ID,
}, },
{ {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(pressureValue), Value: float64(pressureValue),
ValueType: "pressure", ValueType: "pressure",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: bme280.ID, SensorID: bme280.ID,
}, },
{ {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(temperatureValue), Value: float64(temperatureValue),
ValueType: "temperature", ValueType: "temperature",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: bme280.ID, SensorID: bme280.ID,
}, },
} }

View File

@ -43,16 +43,14 @@ func (dht11 *DHT11) Read() ([]*types.MeasuredValue, error) {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(humidityValue), Value: float64(humidityValue),
ValueType: "humidity", ValueType: "humidity",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: dht11.ID, SensorID: dht11.ID,
}, },
{ {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(temperatureValue), Value: float64(temperatureValue),
ValueType: "temperature", ValueType: "temperature",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: dht11.ID, SensorID: dht11.ID,
}, },
} }

View File

@ -43,16 +43,14 @@ func (dht22 *DHT22) Read() ([]*types.MeasuredValue, error) {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(humidityValue), Value: float64(humidityValue),
ValueType: "humidity", ValueType: "humidity",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: dht22.ID, SensorID: dht22.ID,
}, },
{ {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(temperatureValue), Value: float64(temperatureValue),
ValueType: "temperature", ValueType: "temperature",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: dht22.ID, SensorID: dht22.ID,
}, },
} }

View File

@ -60,8 +60,7 @@ func (ds18b20 *DS18B20) Read() ([]*types.MeasuredValue, error) {
ID: uuid.NewV4().String(), ID: uuid.NewV4().String(),
Value: float64(temperatureValue), Value: float64(temperatureValue),
ValueType: "temperature", ValueType: "temperature",
FromDate: format.FormatedTime(), Date: format.FormatedTime(),
TillDate: format.FormatedTime(),
SensorID: ds18b20.ID, SensorID: ds18b20.ID,
}, },
} }

View File

@ -0,0 +1,200 @@
package dockerutils
import (
"context"
"fmt"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
)
type Builder struct {
client *Client
containerConfig *container.Config
containerName string
hostConfig *container.HostConfig
networkConfig *network.NetworkingConfig
networks map[string][]string
ports []string
pull bool
waitForHealthy bool
}
// AddEnv to the container
func (builder *Builder) AddEnv(key string, value string) *Builder {
builder.containerConfig.Env = append(builder.containerConfig.Env, fmt.Sprintf("%v=%v", key, value))
return builder
}
// AddLabel to the container
func (builder *Builder) AddLabel(key string, value string) *Builder {
builder.containerConfig.Labels[key] = value
return builder
}
// Env set environment variables to the container
func (builder *Builder) Env(env map[string]string) *Builder {
builder.containerConfig.Labels = make(map[string]string)
for key, value := range env {
builder.containerConfig.Labels[key] = value
}
return builder
}
// Labels set labels to the container
func (builder *Builder) Labels(labels map[string]string) *Builder {
builder.containerConfig.Labels = labels
return builder
}
// Memory defines a memory limit for the container
func (builder *Builder) Memory(limit int64) *Builder {
builder.hostConfig.Memory = limit
return builder
}
// Mount a source volume or hostpath into the filesystem of the container
func (builder *Builder) Mount(source string, dest string) *Builder {
builder.hostConfig.Binds = append(builder.hostConfig.Binds, fmt.Sprintf("%v:%v", source, dest))
return builder
}
// Mounts a set of source volumes or hostpath into the filesystem of the
// container
func (builder *Builder) Mounts(mounts map[string]string) *Builder {
for source, dest := range mounts {
builder.Mount(source, dest)
}
return builder
}
// Port defines a port forwarding from the host machine to the container
// Examples:
// - 8080:8080
// - 10.6.231.10:8080:8080
// - 10.6.231.10:8080:8080/tcp
func (builder *Builder) Port(port string) *Builder {
builder.ports = append(builder.ports, port)
return builder
}
// Ports defines a set port forwarding rules from the host machine to the
// container
// Examples:
// - 8080:8080
// - 10.6.231.10:8080:8080
// - 10.6.231.10:8080:8080/tcp
func (builder *Builder) Ports(ports []string) *Builder {
builder.ports = ports
return builder
}
// Pull the image if absent
func (builder *Builder) Pull() *Builder {
builder.pull = true
return builder
}
// Start the container
func (builder *Builder) Start(ctx context.Context) (string, error) {
// Pull container image if absent
if builder.pull {
err := builder.client.PullQuiet(ctx, builder.containerConfig.Image)
if err != nil {
return "", err
}
}
// check if ports are available
exposedPorts, portBindings, err := nat.ParsePortSpecs(builder.ports)
if err != nil {
return "", fmt.Errorf("Failed to parse ports %v: %v", builder.ports, err)
}
if len(portBindings) > 0 {
builder.containerConfig.ExposedPorts = exposedPorts
builder.hostConfig.PortBindings = portBindings
}
// add endpoint settings to existing networks
networkExist := make(map[string]bool, 0)
for networkName := range builder.networks {
networkExist[networkName] = false
}
networks, err := builder.client.NetworkList(ctx, types.NetworkListOptions{})
if err != nil {
return "", fmt.Errorf("Failed to list networks: %v", err)
}
for _, nw := range networks {
if aliases, present := builder.networks[nw.Name]; present {
networkExist[nw.Name] = true
builder.networkConfig.EndpointsConfig[nw.Name] = &network.EndpointSettings{
Aliases: aliases,
NetworkID: nw.ID,
}
}
}
for nw, found := range networkExist {
if !found {
return "", fmt.Errorf("Failed to add endpoint settings for network %v. It does not exist", nw)
}
}
// create container
resp, err := builder.client.ContainerCreate(ctx, builder.containerConfig, builder.hostConfig, builder.networkConfig, builder.containerName)
if err != nil {
return "", fmt.Errorf("Failed to create container %v: %v", builder.containerName, err)
}
// start container
err = builder.client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
if err != nil {
stopError := builder.client.ContainerStopByIDs(ctx, time.Second, resp.ID)
if stopError != nil {
return "", fmt.Errorf("Failed to start container %v: %v\nUnable to remove container %v. Manual cleanup necessary", builder.containerName, err, builder.containerName)
}
return "", fmt.Errorf("Failed to start container %v: %v", builder.containerName, err)
}
// wait for healthy
if builder.waitForHealthy {
watcher := builder.client.GetWatcher()
errorChannel := make(chan error, 1)
doneChannel := make(chan struct{})
err = watcher.AddListener(resp.ID, errorChannel, doneChannel)
if err != nil {
containerRemoveError := builder.client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true})
if containerRemoveError != nil {
return "", fmt.Errorf("Failed while watching status for container %v: %v\nUnable to remove container %v: %v", resp.ID, err, resp.ID, containerRemoveError)
}
return "", fmt.Errorf("Failed while watching status for container %v: %v", resp.ID, err)
}
select {
case err := <-errorChannel:
if err != nil {
containerRemoveError := builder.client.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{Force: true})
if containerRemoveError != nil {
return "", fmt.Errorf("Unable to remove container %v: %v", resp.ID, containerRemoveError)
}
return "", err
}
case <-doneChannel:
}
}
return resp.ID, nil
}
func (builder *Builder) WaitForHealthy() *Builder {
builder.waitForHealthy = true
return builder
}

View File

@ -0,0 +1,220 @@
package dockerutils
import (
"context"
"fmt"
"io"
"io/ioutil"
"strings"
"sync"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)
type Client struct {
*client.Client
watcher *Watcher
mutex *sync.Mutex
}
// Close docker connection
func (client *Client) Close() error {
client.mutex.Lock()
defer client.mutex.Unlock()
if client.watcher != nil {
client.watcher.stop()
}
return client.Close()
}
// ContainerListByLabels returns only containers which match by given labels
func (client *Client) ContainerListByLabels(ctx context.Context, all bool, labels map[string]string) ([]types.Container, error) {
filterArgs := filters.NewArgs()
for key, value := range labels {
filterArgs.Add("label", fmt.Sprintf("%v=%v", key, value))
}
containers, err := client.ContainerList(ctx, types.ContainerListOptions{
All: all,
Filters: filterArgs,
})
if err != nil {
return nil, err
}
if containers == nil {
return nil, fmt.Errorf("No containers found by given labels")
}
return containers, nil
}
// ContainerListByNames returns only containers which match by given labels
func (client *Client) ContainerListByNames(ctx context.Context, all bool, names ...string) ([]types.Container, error) {
filterArgs := filters.NewArgs()
for _, name := range names {
filterArgs.Add("name", name)
}
containers, err := client.ContainerList(ctx, types.ContainerListOptions{
All: all,
Filters: filterArgs,
})
if err != nil {
return nil, err
}
if containers == nil {
return nil, fmt.Errorf("No containers found by given names")
}
return containers, nil
}
// ContainerRemoveByIDs deletes all containers which match by their container ids
func (client *Client) ContainerRemoveByIDs(ctx context.Context, containerIDs ...string) error {
for _, containerID := range containerIDs {
err := client.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{Force: true})
if err != nil {
return err
}
}
return nil
}
// ContainerRemoveByLabels deletes all containers which match by given labels
func (client *Client) ContainerRemoveByLabels(ctx context.Context, labels map[string]string) error {
containers, err := client.ContainerListByLabels(ctx, true, labels)
if err != nil {
return err
}
for _, container := range containers {
err := client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
return err
}
}
return nil
}
// ContainerRemoveByNames deletes all containers which match by their names
func (client *Client) ContainerRemoveByNames(ctx context.Context, names ...string) error {
containers, err := client.ContainerListByNames(ctx, true, names...)
if err != nil {
return err
}
for _, container := range containers {
err := client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
return err
}
}
return nil
}
// ContainerStopByIDs deletes all containers which match by their container ids
func (client *Client) ContainerStopByIDs(ctx context.Context, timeout time.Duration, containerIDs ...string) error {
for _, containerID := range containerIDs {
err := client.ContainerStop(ctx, containerID, &timeout)
if err != nil {
return err
}
}
return nil
}
// ContainerStopByLabels shutdown containters which match by given labels
func (client *Client) ContainerStopByLabels(ctx context.Context, timeout time.Duration, labels map[string]string) error {
containers, err := client.ContainerListByLabels(ctx, true, labels)
if err != nil {
return err
}
for _, container := range containers {
err := client.ContainerStop(ctx, container.ID, &timeout)
if err != nil {
return err
}
}
return nil
}
// GetWatcher returns a watcher for container health states
func (client *Client) GetWatcher() *Watcher {
if client.watcher != nil {
return client.watcher
}
client.mutex.Lock()
defer client.mutex.Unlock()
client.watcher = &Watcher{
client: client,
errorChannels: make(map[string]chan<- error),
doneChannels: make(map[string]chan<- struct{}),
errorMapper: make(map[string]ErrorMapper),
mutex: new(sync.RWMutex),
}
client.watcher.start()
return client.watcher
}
// NewBuilder returns a new builder for containers
func (client *Client) NewBuilder(image string) *Builder {
return &Builder{
client: client,
containerConfig: &container.Config{
Image: image,
},
hostConfig: new(container.HostConfig),
networkConfig: new(network.NetworkingConfig),
networks: make(map[string][]string, 0),
ports: make([]string, 0),
pull: false,
waitForHealthy: false,
}
}
// Pull image
func (client *Client) Pull(ctx context.Context, image string, w io.Writer) error {
parts := strings.Split(image, "/")
switch len(parts) {
case 1:
image = fmt.Sprintf("docker.io/library/%v", parts[0])
case 2:
if strings.Compare(parts[0], "library") == 0 ||
strings.Compare(parts[0], "docker.io") == 0 {
image = fmt.Sprintf("docker.io/library/%v", parts[1])
}
}
readCloser, err := client.ImagePull(ctx, image, types.ImagePullOptions{})
if err != nil {
return err
}
_, err = io.Copy(w, readCloser)
if err != nil {
return err
}
return nil
}
// PullQuiet image
func (client *Client) PullQuiet(ctx context.Context, image string) error {
return client.Pull(ctx, image, ioutil.Discard)
}
// New returns a new dockerutil client
func New() (*Client, error) {
dockerClient, err := client.NewEnvClient()
if err != nil {
return nil, err
}
return &Client{
dockerClient,
nil,
new(sync.Mutex),
}, nil
}

View File

@ -0,0 +1,164 @@
package dockerutils
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
)
var (
ErrDied = errors.New("died")
ErrUnhealthy = errors.New("went unhealthy")
)
type Watcher struct {
client *Client
errorChannels map[string]chan<- error
doneChannels map[string]chan<- struct{}
errorMapper map[string]ErrorMapper
mutex *sync.RWMutex
lastError error
cancelFunc context.CancelFunc
}
func (watcher *Watcher) AddListenerWithErrorMapper(containerID string, errorChannel chan<- error, doneChannel chan<- struct{}, errorMapper ErrorMapper) error {
watcher.mutex.Lock()
defer watcher.mutex.Unlock()
if watcher.lastError != nil {
return watcher.lastError
}
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(20*time.Second))
defer cancel()
containerJSON, err := watcher.client.ContainerInspect(ctx, containerID)
if err != nil {
return fmt.Errorf("Unable to check if container is already unhealthy: %v", err)
}
if containerJSON.State.Dead || !containerJSON.State.Running {
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", containerID, ErrDied))
doneChannel <- struct{}{}
}()
return nil
}
if containerJSON.State.Health == nil {
go func() {
doneChannel <- struct{}{}
}()
return nil
}
switch containerJSON.State.Health.Status {
case "starting":
watcher.errorChannels[containerID] = errorChannel
watcher.doneChannels[containerID] = doneChannel
watcher.errorMapper[containerID] = errorMapper
case "healthy":
go func() {
doneChannel <- struct{}{}
}()
case "unhealthy":
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", containerID, ErrUnhealthy))
doneChannel <- struct{}{}
}()
default:
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v went in an unknown state during startup: %s", containerID, containerJSON.State.Health.Status))
doneChannel <- struct{}{}
}()
}
return nil
}
func (watcher *Watcher) AddListener(containerID string, errorChannel chan<- error, doneChannel chan<- struct{}) error {
return watcher.AddListenerWithErrorMapper(containerID, errorChannel, doneChannel, defaultErrorMapper)
}
func (watcher *Watcher) removeListener(containerID string) {
watcher.mutex.Lock()
defer watcher.mutex.Unlock()
delete(watcher.doneChannels, containerID)
delete(watcher.errorChannels, containerID)
delete(watcher.errorMapper, containerID)
}
func (watcher *Watcher) start() {
ctx, cancel := context.WithCancel(context.Background())
watcher.cancelFunc = cancel
dockerEvents, errors := watcher.client.Events(ctx, types.EventsOptions{})
sendErrorFunc := func() {
watcher.mutex.Lock()
for containerID, errorChannel := range watcher.errorChannels {
go func(errorChannel chan<- error, doneChannel chan<- struct{}, err error, errorMapper ErrorMapper) {
errorChannel <- errorMapper(err)
doneChannel <- struct{}{}
}(errorChannel, watcher.doneChannels[containerID], watcher.lastError, watcher.errorMapper[containerID])
}
watcher.mutex.Unlock()
}
go func() {
for {
select {
case event := <-dockerEvents:
watcher.mutex.RLock()
errorChannel, present := watcher.errorChannels[event.Actor.ID]
if !present || event.Type != events.ContainerEventType {
continue
}
doneChannel := watcher.doneChannels[event.Actor.ID]
errorMapper := watcher.errorMapper[event.Actor.ID]
watcher.mutex.RUnlock()
switch event.Action {
case "health_status: healthy":
go func() {
doneChannel <- struct{}{}
}()
watcher.removeListener(event.Actor.ID)
case "health_status: unhealthy":
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", event.Actor.ID, ErrUnhealthy))
doneChannel <- struct{}{}
}()
watcher.removeListener(event.Actor.ID)
case "die":
go func() {
errorChannel <- errorMapper(fmt.Errorf("Container %v: %w", event.Actor.ID, ErrDied))
doneChannel <- struct{}{}
}()
watcher.removeListener(event.Actor.ID)
}
case err := <-errors:
watcher.lastError = err
sendErrorFunc()
return
case <-ctx.Done():
watcher.lastError = fmt.Errorf("Watcher was canceled")
sendErrorFunc()
return
}
}
}()
}
func (watcher *Watcher) stop() {
watcher.cancelFunc()
}
type ErrorMapper func(error) error
func defaultErrorMapper(err error) error {
return err
}

View File

@ -7,6 +7,5 @@ type Device struct {
ID string `json:"id" xml:"id"` ID string `json:"id" xml:"id"`
Name string `json:"name" xml:"name"` Name string `json:"name" xml:"name"`
Location *string `json:"location" xml:"location"` Location *string `json:"location" xml:"location"`
LastContact *time.Time `json:"last_contact" xml:"last_contact"`
CreationDate time.Time `json:"creation_date" xml:"creation_date"` CreationDate time.Time `json:"creation_date" xml:"creation_date"`
} }

View File

@ -11,8 +11,7 @@ type MeasuredValue struct {
ID string `json:"id" xml:"id"` ID string `json:"id" xml:"id"`
Value float64 `json:"value,string" xml:"value,string"` Value float64 `json:"value,string" xml:"value,string"`
ValueType string `json:"value_type" xml:"value_type"` ValueType string `json:"value_type" xml:"value_type"`
FromDate time.Time `json:"from_date" xml:"from_date"` Date time.Time `json:"date" xml:"date"`
TillDate time.Time `json:"till_date" xml:"till_date"`
SensorID string `json:"sensor_id" xml:"sensor_id"` SensorID string `json:"sensor_id" xml:"sensor_id"`
CreationDate *time.Time `json:"creation_date" xml:"creation_date"` CreationDate *time.Time `json:"creation_date" xml:"creation_date"`
UpdateDate *time.Time `json:"update_date" xml:"update_date"` UpdateDate *time.Time `json:"update_date" xml:"update_date"`

View File

@ -16,7 +16,6 @@ type Sensor struct {
GPIONumber string `json:"gpio_number" xml:"gpio_number"` GPIONumber string `json:"gpio_number" xml:"gpio_number"`
Model string `json:"model" xml:"model"` Model string `json:"model" xml:"model"`
Enabled bool `json:"enabled" xml:"enabled"` Enabled bool `json:"enabled" xml:"enabled"`
LastContact *time.Time `json:"last_contact" xml:"last_contact"`
TickDuration string `json:"tick_duration" xml:"tick_duration"` TickDuration string `json:"tick_duration" xml:"tick_duration"`
DeviceID string `json:"device_id" xml:"device_id"` DeviceID string `json:"device_id" xml:"device_id"`
CreationDate time.Time `json:"creation_date" xml:"creation_date"` CreationDate time.Time `json:"creation_date" xml:"creation_date"`