diff --git a/.gitignore b/.gitignore index dcf0649..bb571c2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,6 @@ spaceapid spaceapid-state.json # Config shards -config-credentials.json -config-dynamic.json -config-response.json +credentials.json +dynamic.json +response.json diff --git a/README.md b/README.md index dd1839a..f68787a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ `spaceapid` serves a [SpaceAPI](https://spaceapi.io)-compatible JSON on port 8080: ```shell -$ curl http://[::1]:8080 | jq +$ curl -X GET 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,18 +37,12 @@ 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 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. +Currently only `temperature` and `humidity` are implemented. ```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. @@ -61,5 +55,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 config-credentials.json,config-dynamic.json,config-response.json +go run . -c credentials.json,dynamic.json,response.json ``` diff --git a/config-template.json b/config-template.json index 31cb500..c397fd9 100644 --- a/config-template.json +++ b/config-template.json @@ -46,16 +46,6 @@ "club-assistant" ] } - ], - "beverage_supply": [ - { - "sensor_data": { - "unit": "btl" - }, - "allowed_credentials": [ - "club-assistant" - ] - } ] }, "state": { @@ -76,8 +66,8 @@ "url": "https://hamburg.ccc.de/", "location": { "address": "Zeiseweg 9, 22765 Hamburg, Germany", - "lon": 9.94444, - "lat": 53.5584 + "lon": 9.9445899999999998, + "lat": 53.55836 }, "contact": { "phone": "+49 40 23830150", @@ -89,7 +79,7 @@ }, "feeds": { "blog": { - "type": "application/rss+xml", + "type": "application/atom+xml", "url": "https://hamburg.ccc.de/feed.xml" }, "calendar": { diff --git a/config/config.go b/config/config.go index 2190837..a5258a1 100644 --- a/config/config.go +++ b/config/config.go @@ -10,7 +10,7 @@ import ( "slices" "strings" - "git.hamburg.ccc.de/ccchh/spaceapid/types" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) const ( diff --git a/config/types.go b/config/types.go index 4a305ce..c2b3f12 100644 --- a/config/types.go +++ b/config/types.go @@ -1,7 +1,7 @@ package config import ( - "git.hamburg.ccc.de/ccchh/spaceapid/types" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) type HTTPBACredentialID string diff --git a/go.mod b/go.mod index e3d228e..b0eee8a 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module git.hamburg.ccc.de/ccchh/spaceapid +module gitlab.hamburg.ccc.de/ccchh/spaceapid go 1.21 diff --git a/handlers/root.go b/handlers/root.go index 47091d5..f68f5fd 100644 --- a/handlers/root.go +++ b/handlers/root.go @@ -5,7 +5,7 @@ import ( "log" "net/http" - "git.hamburg.ccc.de/ccchh/spaceapid/types" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) func Root(resp *types.SpaceAPIResponseV14) func(http.ResponseWriter, *http.Request) { diff --git a/handlers/sensors.go b/handlers/sensors.go index 187620e..bd881fb 100644 --- a/handlers/sensors.go +++ b/handlers/sensors.go @@ -7,8 +7,8 @@ import ( "strconv" "time" - "git.hamburg.ccc.de/ccchh/spaceapid/config" - "git.hamburg.ccc.de/ccchh/spaceapid/types" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) func EnvironmentSensor( diff --git a/handlers/state.go b/handlers/state.go index f965db4..0ec9727 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -6,8 +6,8 @@ import ( "strconv" "time" - "git.hamburg.ccc.de/ccchh/spaceapid/config" - "git.hamburg.ccc.de/ccchh/spaceapid/types" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) func StateOpen( diff --git a/handlers/util.go b/handlers/util.go index 890d983..f29e8cc 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -6,8 +6,8 @@ import ( "io" "net/http" - "git.hamburg.ccc.de/ccchh/spaceapid/config" - "git.hamburg.ccc.de/ccchh/spaceapid/util" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/util" ) // updateEndpointValidator checks BasicAuth credentials, diff --git a/main.go b/main.go index 8e4b013..1ec02a5 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,18 @@ package main import ( "flag" + "fmt" "log" "net/http" "os" "os/signal" + "strings" "syscall" - "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" + "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" ) var ( @@ -38,7 +39,7 @@ func main() { conf := config.ParseConfiguration() // Merge old state if present - persistence.MergeOldState(&conf.Response) + util.MergeOldState(&conf.Response) // Register signal handler sc := make(chan os.Signal, 1) @@ -46,7 +47,7 @@ func main() { go func(resp *types.SpaceAPIResponseV14) { <-sc log.Println("Saving state and shutting down...") - persistence.SaveCurrentState(*resp) + util.SaveCurrentState(*resp) os.Exit(0) }(&conf.Response) @@ -58,15 +59,15 @@ func main() { handlers.StateOpen(conf.Credentials, conf.Dynamic.State.Open.AllowedCredentials, &conf.Response.State), ) // Register handlers for Environmental Sensors - for sensorType, envSensorConfigs := range conf.Dynamic.Sensors { + for key, envSensorConfigs := range conf.Dynamic.Sensors { for i, envSensorConfig := range envSensorConfigs { - urlPath := util.GetSensorURLPath( - sensorType, envSensorConfig.SensorData.Location, envSensorConfig.SensorData.Name, - ) - http.HandleFunc( - urlPath, + pattern := fmt.Sprintf("/sensors/%s/%s", key, envSensorConfig.SensorData.Location) + if envSensorConfig.SensorData.Name != "" { + pattern += "/" + envSensorConfig.SensorData.Name + } + http.HandleFunc(strings.ToLower(pattern), handlers.EnvironmentSensor( - conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[sensorType][i], + conf.Credentials, envSensorConfig.AllowedCredentials, &conf.Response.Sensors[key][i], ), ) } diff --git a/persistence/persistentState.go b/persistence/persistentState.go deleted file mode 100644 index 01e1912..0000000 --- a/persistence/persistentState.go +++ /dev/null @@ -1,119 +0,0 @@ -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) - } -} diff --git a/persistence/persistentStateDir.go b/persistence/persistentStateDir.go deleted file mode 100644 index 432dcff..0000000 --- a/persistence/persistentStateDir.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !linux - -package persistence - -const ( - persistentStateDir = "./" -) diff --git a/persistence/persistentStateDir_linux.go b/persistence/persistentStateDir_linux.go deleted file mode 100644 index 65c0f8d..0000000 --- a/persistence/persistentStateDir_linux.go +++ /dev/null @@ -1,5 +0,0 @@ -package persistence - -const ( - persistentStateDir = "/var/lib/spaceapid/" -) diff --git a/types/v14.go b/types/v14.go index 2492639..2bcdc06 100644 --- a/types/v14.go +++ b/types/v14.go @@ -45,7 +45,7 @@ type SpaceState struct { type EnvironmentSensor struct { Value float64 `json:"value"` Unit string `json:"unit"` - Location string `json:"location,omitempty"` + Location string `json:"location"` 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,omitempty"` + Location string `json:"location"` Name string `json:"name,omitempty"` LastChange int64 `json:"lastchange"` } diff --git a/util/credentials.go b/util/credentials.go index f515e61..9352d5e 100644 --- a/util/credentials.go +++ b/util/credentials.go @@ -1,7 +1,7 @@ package util import ( - "git.hamburg.ccc.de/ccchh/spaceapid/config" + "gitlab.hamburg.ccc.de/ccchh/spaceapid/config" ) // CheckCredentials validates whether a given username/password pair matches an diff --git a/util/util.go b/util/util.go index 421f72c..086ee2f 100644 --- a/util/util.go +++ b/util/util.go @@ -1,20 +1,119 @@ package util import ( - "fmt" - "strings" + "encoding/json" + "errors" + "log" + "os" + "path" + + "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" ) -// 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 +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 } - if name != "" { - path += "/" + name + + // 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) } - return strings.ToLower(path) }