diff --git a/.gitignore b/.gitignore index bb571c2..dcf0649 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index f68787a..dd1839a 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 -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 ``` diff --git a/config-template.json b/config-template.json index c397fd9..31cb500 100644 --- a/config-template.json +++ b/config-template.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": { diff --git a/config/config.go b/config/config.go index a5258a1..2190837 100644 --- a/config/config.go +++ b/config/config.go @@ -10,7 +10,7 @@ import ( "slices" "strings" - "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" + "git.hamburg.ccc.de/ccchh/spaceapid/types" ) const ( diff --git a/config/types.go b/config/types.go index c2b3f12..4a305ce 100644 --- a/config/types.go +++ b/config/types.go @@ -1,7 +1,7 @@ package config import ( - "gitlab.hamburg.ccc.de/ccchh/spaceapid/types" + "git.hamburg.ccc.de/ccchh/spaceapid/types" ) type HTTPBACredentialID string diff --git a/go.mod b/go.mod index b0eee8a..e3d228e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module gitlab.hamburg.ccc.de/ccchh/spaceapid +module git.hamburg.ccc.de/ccchh/spaceapid go 1.21 diff --git a/handlers/root.go b/handlers/root.go index f68f5fd..47091d5 100644 --- a/handlers/root.go +++ b/handlers/root.go @@ -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) { diff --git a/handlers/sensors.go b/handlers/sensors.go index bd881fb..187620e 100644 --- a/handlers/sensors.go +++ b/handlers/sensors.go @@ -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( diff --git a/handlers/state.go b/handlers/state.go index 0ec9727..f965db4 100644 --- a/handlers/state.go +++ b/handlers/state.go @@ -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( diff --git a/handlers/util.go b/handlers/util.go index f29e8cc..890d983 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -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, diff --git a/main.go b/main.go index 1ec02a5..8e4b013 100644 --- a/main.go +++ b/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], ), ) } diff --git a/persistence/persistentState.go b/persistence/persistentState.go new file mode 100644 index 0000000..01e1912 --- /dev/null +++ b/persistence/persistentState.go @@ -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) + } +} diff --git a/persistence/persistentStateDir.go b/persistence/persistentStateDir.go new file mode 100644 index 0000000..432dcff --- /dev/null +++ b/persistence/persistentStateDir.go @@ -0,0 +1,7 @@ +//go:build !linux + +package persistence + +const ( + persistentStateDir = "./" +) diff --git a/persistence/persistentStateDir_linux.go b/persistence/persistentStateDir_linux.go new file mode 100644 index 0000000..65c0f8d --- /dev/null +++ b/persistence/persistentStateDir_linux.go @@ -0,0 +1,5 @@ +package persistence + +const ( + persistentStateDir = "/var/lib/spaceapid/" +) diff --git a/types/v14.go b/types/v14.go index 2bcdc06..2492639 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"` + 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"` } diff --git a/util/credentials.go b/util/credentials.go index 9352d5e..f515e61 100644 --- a/util/credentials.go +++ b/util/credentials.go @@ -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 diff --git a/util/util.go b/util/util.go index 086ee2f..421f72c 100644 --- a/util/util.go +++ b/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) }