Refactoring #34
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -11,6 +11,6 @@ spaceapid
|
|||
spaceapid-state.json
|
||||
|
||||
# Config shards
|
||||
credentials.json
|
||||
dynamic.json
|
||||
response.json
|
||||
config-credentials.json
|
||||
config-dynamic.json
|
||||
config-response.json
|
||||
|
|
16
README.md
16
README.md
|
@ -3,7 +3,7 @@
|
|||
`spaceapid` serves a [SpaceAPI](https://spaceapi.io)-compatible JSON on port 8080:
|
||||
|
||||
```shell
|
||||
$ curl -X GET http://[::1]:8080 | jq
|
||||
$ curl http://[::1]:8080 | jq
|
||||
{
|
||||
"api_compatibility": [
|
||||
"14"
|
||||
|
@ -26,7 +26,7 @@ The config consists of three parts:
|
|||
- `"response"`
|
||||
- The static (pre-filled) parts of the response
|
||||
|
||||
See [Running](#running) for details.
|
||||
See [Running](#Running) for details.
|
||||
|
||||
## Updating values
|
||||
|
||||
|
@ -37,12 +37,18 @@ curl -X PUT -u user:password -d true http://[::1]:8080/state/open
|
|||
```
|
||||
|
||||
The same is true for the endpoints for sensors configured under `"dynamic"`.
|
||||
Currently only `temperature` and `humidity` are implemented.
|
||||
Currently only the sensors with the `value/unit/location/name/description` schema are implemented.
|
||||
At the time of writing this includes `temperature`, `barometer`, `humidity`, `beverage_supply`, `power_consumption`,
|
||||
and `account_balance`.
|
||||
Out-of-spec sensors may also be used as long as they share the same schema.
|
||||
|
||||
```shell
|
||||
curl -X PUT -u user:password -d 23.42 http://[::1]:8080/sensors/{temperature,humidity}/location[/name]
|
||||
curl -X PUT -u user:password -d 23.42 http://[::1]:8080/sensors/{temperature,humidity,...}[/location[/name]]
|
||||
```
|
||||
|
||||
As can be seen in the example, the http urls are generated from sensor type and optionally `location` and `name`.
|
||||
Depending on sensor type `location` might be required for your sensors, see the schema for details.
|
||||
|
||||
## Building
|
||||
|
||||
See the `go.mod` file for minimum required Go version.
|
||||
|
@ -55,5 +61,5 @@ Set the environment variable to a comma-separated list of config files or pass t
|
|||
```shell
|
||||
env CONFIG_PATH=config-template.json go run .
|
||||
# OR
|
||||
go run . -c credentials.json,dynamic.json,response.json
|
||||
go run . -c config-credentials.json,config-dynamic.json,config-response.json
|
||||
```
|
||||
|
|
|
@ -46,6 +46,16 @@
|
|||
"club-assistant"
|
||||
]
|
||||
}
|
||||
],
|
||||
"beverage_supply": [
|
||||
{
|
||||
"sensor_data": {
|
||||
"unit": "btl"
|
||||
},
|
||||
"allowed_credentials": [
|
||||
"club-assistant"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
|
@ -66,8 +76,8 @@
|
|||
"url": "https://hamburg.ccc.de/",
|
||||
"location": {
|
||||
"address": "Zeiseweg 9, 22765 Hamburg, Germany",
|
||||
"lon": 9.9445899999999998,
|
||||
"lat": 53.55836
|
||||
"lon": 9.94444,
|
||||
"lat": 53.5584
|
||||
},
|
||||
"contact": {
|
||||
"phone": "+49 40 23830150",
|
||||
|
@ -79,7 +89,7 @@
|
|||
},
|
||||
"feeds": {
|
||||
"blog": {
|
||||
"type": "application/atom+xml",
|
||||
"type": "application/rss+xml",
|
||||
"url": "https://hamburg.ccc.de/feed.xml"
|
||||
},
|
||||
"calendar": {
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"slices"
|
||||
"strings"
|
||||
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
)
|
||||
|
||||
type HTTPBACredentialID string
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
|||
module gitlab.hamburg.ccc.de/ccchh/spaceapid
|
||||
module git.hamburg.ccc.de/ccchh/spaceapid
|
||||
|
||||
go 1.21
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
)
|
||||
|
||||
func Root(resp *types.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Request) {
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
)
|
||||
|
||||
func EnvironmentSensor(
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
)
|
||||
|
||||
func StateOpen(
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/util"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/util"
|
||||
)
|
||||
|
||||
// updateEndpointValidator checks BasicAuth credentials,
|
||||
|
|
29
main.go
29
main.go
|
@ -2,18 +2,17 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/handlers"
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/util"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/handlers"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/persistence"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -39,7 +38,7 @@ func main() {
|
|||
conf := config.ParseConfiguration()
|
||||
|
||||
// Merge old state if present
|
||||
util.MergeOldState(&conf.Response)
|
||||
persistence.MergeOldState(&conf.Response)
|
||||
|
||||
// Register signal handler
|
||||
sc := make(chan os.Signal, 1)
|
||||
|
@ -47,7 +46,7 @@ func main() {
|
|||
go func(resp *types.SpaceAPIResponseV14) {
|
||||
<-sc
|
||||
log.Println("Saving state and shutting down...")
|
||||
util.SaveCurrentState(*resp)
|
||||
persistence.SaveCurrentState(*resp)
|
||||
os.Exit(0)
|
||||
}(&conf.Response)
|
||||
|
||||
|
@ -59,15 +58,15 @@ func main() {
|
|||
handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State),
|
||||
)
|
||||
// Register handlers for Environmental Sensors
|
||||
for key, envSensorConfigs := range conf.Dynamic.Sensors {
|
||||
for sensorType, envSensorConfigs := range conf.Dynamic.Sensors {
|
||||
for i, envSensorConfig := range envSensorConfigs {
|
||||
pattern := fmt.Sprintf("/sensors/%s/%s", key, envSensorConfig.SensorData.Location)
|
||||
if envSensorConfig.SensorData.Name != "" {
|
||||
pattern += "/" + envSensorConfig.SensorData.Name
|
||||
}
|
||||
http.HandleFunc(strings.ToLower(pattern),
|
||||
urlPath := util.GetSensorURLPath(
|
||||
sensorType, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name,
|
||||
)
|
||||
http.HandleFunc(
|
||||
urlPath,
|
||||
handlers.EnvironmentSensor(
|
||||
conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[key][i],
|
||||
conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
119
persistence/persistentState.go
Normal file
119
persistence/persistentState.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package persistence
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
)
|
||||
|
||||
const (
|
||||
persistentStateFile = "spaceapid-state.json"
|
||||
persistentStatePath = persistentStateDir + persistentStateFile
|
||||
)
|
||||
|
||||
// MergeOldState merges a given SpaceAPIResponse with the state saved at the time of last program exit and then deletes
|
||||
// the file containing the old state.
|
||||
func MergeOldState(response *types.SpaceAPIResponseV14) {
|
||||
var (
|
||||
err error
|
||||
oldState []byte
|
||||
persistedState types.PersistentStateV14
|
||||
)
|
||||
|
||||
log.Println("Merging old state from", persistentStatePath, "...")
|
||||
|
||||
// Check if state.json is present
|
||||
_, err = os.Stat(persistentStatePath)
|
||||
if err != nil {
|
||||
log.Println("Old state json not accessible at", persistentStatePath, ", skipping merge... error:", err)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
goto removeOld
|
||||
}
|
||||
|
||||
// Read file and load persisted state
|
||||
oldState, err = os.ReadFile(persistentStatePath)
|
||||
if err != nil {
|
||||
log.Println("Error reading old state from", persistentStatePath, ", skipping merge... error:", err)
|
||||
goto removeOld
|
||||
}
|
||||
err = json.Unmarshal(oldState, &persistedState)
|
||||
if err != nil {
|
||||
log.Println(persistentStatePath, "doesn't seem to contain valid data... error:", err)
|
||||
goto removeOld
|
||||
}
|
||||
|
||||
// Merge state
|
||||
response.State = persistedState.State
|
||||
|
||||
// Merge sensors
|
||||
for key, environmentSensors := range persistedState.Sensors {
|
||||
for _, sensor := range environmentSensors {
|
||||
// Order or amount of sensors might have changed, so check sensors already present in response
|
||||
// and then look for matching values in persisted state
|
||||
for i := range response.Sensors[key] {
|
||||
if rs := &response.Sensors[key][i]; rs.Location == sensor.Location && rs.Name == sensor.Name {
|
||||
rs.Value = sensor.Value
|
||||
rs.LastChange = sensor.LastChange
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old state json
|
||||
removeOld:
|
||||
err = os.RemoveAll(persistentStatePath)
|
||||
if err != nil {
|
||||
log.Println("Failed to remove", persistentStatePath, ", continuing... error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func SaveCurrentState(response types.SpaceAPIResponseV14) {
|
||||
// Create state directory if not present
|
||||
err := os.MkdirAll(persistentStateDir, 0750)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed creating", persistentStateDir, ", aborting... error:", err)
|
||||
}
|
||||
|
||||
// Open persistent state file
|
||||
file, err := os.OpenFile(persistentStatePath, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed opening", persistentStatePath, "while trying to save current state... error:", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Create persistent state
|
||||
persistentStateV14 := types.PersistentStateV14{
|
||||
State: struct {
|
||||
Open bool `json:"open"`
|
||||
LastChange int64 `json:"lastchange"`
|
||||
}{Open: response.State.Open, LastChange: response.State.LastChange},
|
||||
}
|
||||
// Save sensor state
|
||||
persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor)
|
||||
for key, environmentSensors := range response.Sensors {
|
||||
persistentStateV14.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors))
|
||||
for i, sensor := range environmentSensors {
|
||||
persistentStateV14.Sensors[key][i].Value = sensor.Value
|
||||
persistentStateV14.Sensors[key][i].Location = sensor.Location
|
||||
persistentStateV14.Sensors[key][i].Name = sensor.Name
|
||||
persistentStateV14.Sensors[key][i].LastChange = sensor.LastChange
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize persistent state
|
||||
marshal, err := json.MarshalIndent(persistentStateV14, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed serializing persistent state... error:", err)
|
||||
}
|
||||
|
||||
// Write to file
|
||||
_, err = file.Write(marshal)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed writing persistent state to file", file.Name(), "... error:", err)
|
||||
}
|
||||
}
|
7
persistence/persistentStateDir.go
Normal file
7
persistence/persistentStateDir.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
//go:build !linux
|
||||
|
||||
package persistence
|
||||
|
||||
const (
|
||||
persistentStateDir = "./"
|
||||
)
|
5
persistence/persistentStateDir_linux.go
Normal file
5
persistence/persistentStateDir_linux.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package persistence
|
||||
|
||||
const (
|
||||
persistentStateDir = "/var/lib/spaceapid/"
|
||||
)
|
|
@ -45,7 +45,7 @@ type SpaceState struct {
|
|||
type EnvironmentSensor struct {
|
||||
Value float64 `json:"value"`
|
||||
Unit string `json:"unit"`
|
||||
Location string `json:"location"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
LastChange int64 `json:"lastchange,omitempty"`
|
||||
|
@ -61,7 +61,7 @@ type PersistentStateV14 struct {
|
|||
|
||||
type PersistentEnvironmentSensor struct {
|
||||
Value float64 `json:"value"`
|
||||
Location string `json:"location"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
LastChange int64 `json:"lastchange"`
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
"git.hamburg.ccc.de/ccchh/spaceapid/config"
|
||||
)
|
||||
|
||||
// CheckCredentials validates whether a given username/password pair matches an
|
||||
|
|
123
util/util.go
123
util/util.go
|
@ -1,119 +1,20 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"gitlab.hamburg.ccc.de/ccchh/spaceapid/types"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
savedStateJSONPath = "/var/lib/spaceapid/spaceapid-state.json"
|
||||
)
|
||||
|
||||
// MergeOldState merges a given SpaceAPIResponse with the state saved at the time of last program exit and then deletes
|
||||
// the file containing the old state.
|
||||
func MergeOldState(response *types.SpaceAPIResponseV14) {
|
||||
var (
|
||||
err error
|
||||
oldState []byte
|
||||
persistedState types.PersistentStateV14
|
||||
)
|
||||
|
||||
log.Println("Merging old state from", savedStateJSONPath, "...")
|
||||
|
||||
// Check if state.json is present
|
||||
_, err = os.Stat(savedStateJSONPath)
|
||||
if err != nil {
|
||||
log.Println("Old state json not accessible at", savedStateJSONPath, ", skipping merge... error:", err)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
goto removeOld
|
||||
// GetSensorURLPath generates the URL path for the given sensor details.
|
||||
// location and name may be optional depending on sensorType, see the schema definition for details.
|
||||
// The path is always all-lowercase.
|
||||
func GetSensorURLPath(sensorType, location, name string) string {
|
||||
path := fmt.Sprintf("/sensors/%s", sensorType)
|
||||
if location != "" {
|
||||
path += "/" + location
|
||||
}
|
||||
|
||||
// Read file and load persisted state
|
||||
oldState, err = os.ReadFile(savedStateJSONPath)
|
||||
if err != nil {
|
||||
log.Println("Error reading old state from", savedStateJSONPath, ", skipping merge... error:", err)
|
||||
goto removeOld
|
||||
}
|
||||
err = json.Unmarshal(oldState, &persistedState)
|
||||
if err != nil {
|
||||
log.Println(savedStateJSONPath, "doesn't seem to contain valid data... error:", err)
|
||||
goto removeOld
|
||||
}
|
||||
|
||||
// Merge state
|
||||
response.State = persistedState.State
|
||||
|
||||
// Merge sensors
|
||||
for key, environmentSensors := range persistedState.Sensors {
|
||||
for _, sensor := range environmentSensors {
|
||||
// Order or amount of sensors might have changed, so check sensors already present in response
|
||||
// and then look for matching values in persisted state
|
||||
for i := range response.Sensors[key] {
|
||||
if rs := &response.Sensors[key][i]; rs.Location == sensor.Location && rs.Name == sensor.Name {
|
||||
rs.Value = sensor.Value
|
||||
rs.LastChange = sensor.LastChange
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old state json
|
||||
removeOld:
|
||||
err = os.RemoveAll(savedStateJSONPath)
|
||||
if err != nil {
|
||||
log.Println("Failed to remove", savedStateJSONPath, ", continuing... error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func SaveCurrentState(response types.SpaceAPIResponseV14) {
|
||||
// Create state directory if not present
|
||||
err := os.MkdirAll(path.Dir(savedStateJSONPath), 0750)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed creating", savedStateJSONPath, ", aborting... error:", err)
|
||||
}
|
||||
|
||||
// Open persistent state file
|
||||
file, err := os.OpenFile(savedStateJSONPath, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed opening", savedStateJSONPath, "while trying to save current state... error:", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Create persistent state
|
||||
persistentStateV14 := types.PersistentStateV14{
|
||||
State: struct {
|
||||
Open bool `json:"open"`
|
||||
LastChange int64 `json:"lastchange"`
|
||||
}{Open: response.State.Open, LastChange: response.State.LastChange},
|
||||
}
|
||||
// Save sensor state
|
||||
persistentStateV14.Sensors = make(map[string][]types.PersistentEnvironmentSensor)
|
||||
for key, environmentSensors := range response.Sensors {
|
||||
persistentStateV14.Sensors[key] = make([]types.PersistentEnvironmentSensor, len(environmentSensors))
|
||||
for i, sensor := range environmentSensors {
|
||||
persistentStateV14.Sensors[key][i].Value = sensor.Value
|
||||
persistentStateV14.Sensors[key][i].Location = sensor.Location
|
||||
persistentStateV14.Sensors[key][i].Name = sensor.Name
|
||||
persistentStateV14.Sensors[key][i].LastChange = sensor.LastChange
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize persistent state
|
||||
marshal, err := json.MarshalIndent(persistentStateV14, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed serializing persistent state... error:", err)
|
||||
}
|
||||
|
||||
// Write to file
|
||||
_, err = file.Write(marshal)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed writing persistent state to file", file.Name(), "... error:", err)
|
||||
if name != "" {
|
||||
path += "/" + name
|
||||
}
|
||||
return strings.ToLower(path)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue