diff --git a/spacepanel_aggregator/conf.yml b/conf.yml similarity index 84% rename from spacepanel_aggregator/conf.yml rename to conf.yml index c09bea5..beb9b07 100644 --- a/spacepanel_aggregator/conf.yml +++ b/conf.yml @@ -1,183 +1,126 @@ -#led0: -- +led0: - https://www.devtal.de/api/ -#led1: -- +led1: - https://www.binary-kitchen.de/spaceapi.php -#led2: -- +led2: - https://status.aachen.ccc.de/spaceapi -#led3: -- +led3: - https://schalter.ccchb.de/spaceapi.json -#led4: -- +led4: - https://api.koeln.ccc.de -#led5: -- +led5: - https://www.ccc-mannheim.de/spaceapi/spaceapi.json -#led6: -- +led6: - http://cccfr.de/status/spaceapi.py -#led7: -- +led7: - http://chaos-consulting.de/api/space.api -#led8: -- +led8: - https://status.chaospott.de/status.json -#led9: -- +led9: - https://chaoschemnitz.de/chch.json -#led10: -- +led10: - https://www.ccc-p.org/spaceapi.json -#led11: -- +led11: - http://doorstatus.c3re.de/status/json -#led12: -- +led12: - http://status.ctdo.de/api/spaceapi/v13 -#led13: -- +led13: - https://status.diyww.de/status.json -#led14: -- +led14: - http://club.entropia.de/spaceapi -#led15: -- +led15: - https://fablab.fau.de/spaceapi/ -#led16: -- +led16: - https://spaceapi.futev.de/spaceapi.json -#led17: -- +led17: - https://freieslabor.org/api/info -#led18: -- +led18: - https://hackerspace-bielefeld.de/spacestatus/status.json -#led19: -- +led19: - https://hacklabor.de/api/space/v1/ -#led20: -- +led20: - http://spaceapi.hacksaar.de/status.json -#led21: -- +led21: - https://status.hasi.it/spaceapi -#led22: -- +led22: - https://status.kraut.space/api -#led23: -- +led23: - http://status.leinelab.org/api/spaceapi.json -#led24: -- +led24: - http://status.mainframe.io/api/spaceInfo -#led25: -- +led25: - http://spaceapi.n39.eu/json -#led26: -- +led26: - http://netzladen.org/api/status.json -#led27: -- +led27: - https://api.nerd2nerd.org/status.json -#led28: -- +led28: - https://cccgoe.de/spaceapi.php -#led29: -- +led29: - http://api.openlab-augsburg.de/data.json -#led30: -- +led30: - https://werkraum.freiraumzittau.de/spaceapi/13/ -#led31: -- +led31: - https://spaceapi.reaktor23.org -#led32: -- +led32: - http://status.stratum0.org/status.json -#led33: -- +led33: - https://api.warpzone.ms/spaceapi -#led34: -- +led34: - https://hsmr.cc/spaceapi.json -#led35: -- +led35: - https://status.bckspc.de/spacestatus.php -#led36: -- +led36: - http://stats.bytewerk.org/status.json -#led37: -- +led37: - https://api.flipdot.org/ -#led38: -- +led38: - https://spaceapi.hackzogtum-coburg.de -#led39: -- +led39: - https://state.maglab.space/spaceapi.json -#led40: -- +led40: - http://nobreakspace.org/status/spaceapi.json -#led41: -- +led41: - https://bodensee.space/spaceapi/see-base.json -#led42: -- +led42: - https://api.shackspace.de/v1/spaceapi -#led43: -- +led43: - https://verschwoerhaus.de/feed/spaceapi -#led44: -- +led44: - https://vspace.one/spaceapi.json -#led45: -- +led45: - https://keinanschluss.un-hack-bar.de/spaceapi.json -#led46: -- +led46: - https://www.hackerspace-sw.de/spaceapi.json -#led47: -- +led47: - https://hamburg.ccc.de/dooris/status.json - http://blog.attraktor.org/spaceapi/spaceapi.json -#led48: -- +led48: - https://status.makerspace-erfurt.de/status.json - http://status.bytespeicher.org/status.json -#led49: -- +led49: - http://status.munichmakerlab.de/spaceapi.php - http://api.muc.ccc.de/spaceapi.json -#led50: -- +led50: - http://api.terminal21.de - http://api.terminal21.de/status_ebk.json -#led51: -- +led51: - https://fnord.istsystemrelevant.de/spaceapi.json - https://chaosdorf.de/space_api.json -#led52: -- +led52: - http://spaceapi.nordlab-ev.de - https://api.chaostreff-flensburg.de/ -#led53: -- +led53: - http://www.space-left.org/spaceapi13.json - https://das-labor.org/status/api -#led54: -- +led54: - http://www.turmlabor.de/spaces.api - https://www.c3d2.de/spaceapi.json -#led55: -- +led55: - http://spaceapi.k4cg.org/spaceapi.json - http://api.fablab-nuernberg.de/spaceapi.php - https://status.nerdberg.de/api/space -#led56: -- +led56: - https://x-hain.de/spaceapi-0.13.json - http://www.c-base.org/status.json - https://spaceapi.motionlab.berlin/ diff --git a/go.mod b/go.mod index 5d38084..9bc6b7f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.13 require ( github.com/gin-gonic/gin v1.5.0 + github.com/sirupsen/logrus v1.4.2 + github.com/spf13/pflag v1.0.5 github.com/valyala/fastjson v1.4.5 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index aced6c8..b93485c 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,7 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= @@ -24,7 +25,11 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -34,6 +39,7 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/valyala/fastjson v1.4.5 h1:uSuLfXk2LzRtzwd3Fy5zGRBe0Vs7zhs11vjdko32xb4= github.com/valyala/fastjson v1.4.5/go.mod h1:nV6MsjxL2IMJQUoHDIrjEI7oLyeqK6aBD7EFWPsvP8o= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9cbc3f3 --- /dev/null +++ b/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "encoding/json" + "fmt" + io "io/ioutil" + "net/http" + "os" + + "github.com/sirupsen/logrus" + flag "github.com/spf13/pflag" + "gopkg.in/yaml.v2" +) + +var ( + configFile string + addr string +) + +func init() { + flag.StringVar(&configFile, "config", "conf.yml", "") + flag.StringVar(&addr, "addr", ":8080", "") +} + +func main() { + flag.Parse() + + if _, err := os.Stat(configFile); err == os.ErrNotExist { + logrus.Fatal("Config file not found") + flag.PrintDefaults() + return + } + + logrus.Println("Welcome to Spacepanel Aggregator!") + logrus.Println() + logrus.Printf("Listen Address: %s", addr) + logrus.Printf("Config-File: %s", configFile) + + bytes, err := io.ReadFile(configFile) + if err != nil { + logrus.Fatal(err) + } + + var ledSpaceMap map[string][]string + err = yaml.Unmarshal(bytes, &ledSpaceMap) + if err != nil { + logrus.Fatalf("Error loading config file: %v", err) + } + + spaceCount := 0 + for _, spaces := range ledSpaceMap { + spaceCount += len(spaces) + } + + fmt.Println("Loaded", len(ledSpaceMap), "LED-configs and", spaceCount, "spaces.") + + aggregator := NewStateAggregator(ledSpaceMap) + + server := http.Server{ + Addr: addr, + } + + http.HandleFunc("/leds", func(writer http.ResponseWriter, request *http.Request) { + data, err := json.Marshal(aggregator.GetLedStates()) + if err != nil { + logrus.Error(err) + writer.WriteHeader(503) + return + } + + writer.Write(data) + }) + + server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logrus.Infof("Request: %s %s %s", r.RemoteAddr, r.Method, r.URL) + http.DefaultServeMux.ServeHTTP(w, r) + }) + + logrus.Fatal(server.ListenAndServe()) +} diff --git a/httppoll.go b/poll.go similarity index 56% rename from httppoll.go rename to poll.go index c5573bc..f714919 100644 --- a/httppoll.go +++ b/poll.go @@ -1,26 +1,19 @@ -package spacepanel_aggregator +package main import ( "crypto/tls" "errors" - "fmt" "io/ioutil" "net/http" "time" + "github.com/sirupsen/logrus" "github.com/valyala/fastjson" ) -func PollWorker(url string, led int) { - for { - err := Worker(url) - if err != nil { - SetSpaceState(url, Unknown) - fmt.Println("LED", led, "URL:", url, err.Error()) - } - time.Sleep(sleeptime) - } -} +const ( + SleepTime = 60 * time.Minute +) var ( insecureTransport = &http.Transport{ @@ -32,45 +25,48 @@ var ( } ) -func Worker(url string) error { +func StartPollWorker(space *Space) { + for { + state, err := requestSpaceState(space.URL) + if err != nil { + logrus.Errorf("URL: %s, Err: %v", space.URL, err) + } + space.State = state + time.Sleep(SleepTime) + } +} + +func requestSpaceState(url string) (State, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { - return err + return Unknown, err } resp, err := client.Do(req) if err != nil { - return err + return Unknown, err } defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - return err + return Unknown, err } value, err := fastjson.ParseBytes(data) if err != nil { - return err + return Unknown, err } if !value.Exists("state", "open") { - return errors.New("no space state existing") + return Unknown, errors.New("no space state existing") } state := value.GetBool("state", "open") if state { - SetSpaceState(url, Open) - } else { - SetSpaceState(url, Close) + return Open, nil } - return nil -} - -func SetSpaceState(url string, s State) { - lock.Lock() - spacestates[url] = s - lock.Unlock() + return Close, nil } diff --git a/setup.go b/setup.go deleted file mode 100644 index 867ffea..0000000 --- a/setup.go +++ /dev/null @@ -1,86 +0,0 @@ -package spacepanel_aggregator - -import ( - "fmt" - "github.com/gin-gonic/gin" - yaml "gopkg.in/yaml.v2" - io "io/ioutil" - "net/http" - "sync" - "time" -) - -var listen = ":8080" -var conffile = "conf.yml" -var sleeptime time.Duration = 60000000000 // nanoseconds -var leds [][]string -var spacestates map[string]State -var lock = sync.RWMutex{} - -func SetConf(s string) { - conffile = s -} - -func SetIf(s string) { - listen = s -} - -func Start() { - fmt.Println("Welcome to Spacepanel Aggregator!\n") - fmt.Println("Listen Interface: ", listen) - fmt.Println("Config-File: ", conffile) - bytes, err := io.ReadFile(conffile) - if err != nil { - ce(err) - } - err = yaml.Unmarshal(bytes, &leds) - - if err != nil { - ce(err) - panic("An error occured while parsing the conffile, quitting...") - } - spacestates = make(map[string]State) - for i := 0; i < len(leds); i++ { - for j := 0; j < len(leds[i]); j++ { - spacestates[leds[i][j]] = Unknown - go PollWorker(leds[i][j], i) - } - } - fmt.Println("Loaded", len(leds), "LED-configs and", len(spacestates), "spaces.") - - r := setupRouter() - _ = r.Run(listen) -} - -func getBestState(states []string) State { - returner := Unknown - for i := 0; i < len(states); i++ { - if spacestates[states[i]] < returner { - returner = spacestates[states[i]] - } - } - return returner -} - -func getLedStates(c *gin.Context) { - returner := make([]string, len(leds)) - for i := 0; i < len(leds); i++ { - returner[i] = colors[getBestState(leds[i])] - } - c.JSON(http.StatusOK, returner) -} - -func setupRouter() *gin.Engine { - gin.SetMode(gin.ReleaseMode) - r := gin.Default() - // Ping test - //r.GET("/leds", getLedStates()) - r.GET("/leds", getLedStates) - return r -} - -func ce(err error) { - if err != nil { - fmt.Println("ERROR: ", err.Error()) - } -} diff --git a/spaceAggregator.go b/spaceAggregator.go new file mode 100644 index 0000000..397b3ee --- /dev/null +++ b/spaceAggregator.go @@ -0,0 +1,75 @@ +package main + +import ( + "sync" +) + +type State int + +const ( + Unknown = State(iota) + Outdated + Close + Open +) + +var ColorMap = map[State]string{ + Unknown: "#000000", + Outdated: "#0000ff", + Close: "#ff0000", + Open: "#00ff00", +} + +type Space struct { + State State + URL string +} + +type StateAggregator struct { + ledList map[int][]*Space + mtx sync.RWMutex +} + +func NewStateAggregator(spaceList map[string][]string) *StateAggregator { + s := &StateAggregator{ + ledList: make(map[int][]*Space), + } + + i := 0 + for _, spaceUrls := range spaceList { + for _, url := range spaceUrls { + space := &Space{ + URL: url, + } + + go StartPollWorker(space) + s.ledList[i] = append(s.ledList[i], space) + } + i++ + } + + return s +} + +func GetBestStateFromList(spaces []*Space) State { + state := Unknown + for _, space := range spaces { + if space.State > state { + state = space.State + } + } + + return state +} + +func (s *StateAggregator) GetLedStates() map[int]string { + states := make(map[int]string) + + s.mtx.RLock() + for i, spaceList := range s.ledList { + states[i] = ColorMap[GetBestStateFromList(spaceList)] + } + s.mtx.RUnlock() + + return states +} diff --git a/spacepanel_aggregator/cmd.go b/spacepanel_aggregator/cmd.go deleted file mode 100644 index 4386c0d..0000000 --- a/spacepanel_aggregator/cmd.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "os" - sa "repos.ctdo.de/mamu/spacepanel_aggregator" -) - -const helptext string = "spacepanel_aggregator usage: \n\n -l IF:port default: *:8080 \n -f config-file default: conf.yml \n -h print help and exit\n" - -func main() { - conf := true - for i := 1; i < len(os.Args); i++ { - switch os.Args[i] { - case "-l": - i++ - if i < len(os.Args) { - sa.SetIf(os.Args[i]) - } else { - conf = false - } - case "-f": - i++ - if i < len(os.Args) { - sa.SetConf(os.Args[i]) - } else { - conf = false - } - default: - conf = false - i = len(os.Args) - } - - } - if conf { - sa.Start() - } else { - printHelp() - } -} - -func printHelp() { - fmt.Printf(helptext) -} diff --git a/types.go b/types.go deleted file mode 100644 index 0a19d37..0000000 --- a/types.go +++ /dev/null @@ -1,239 +0,0 @@ -package spacepanel_aggregator - -type State int - -const ( - Open = State(iota) - Close - Outdated - Unknown -) - -var colors = [4]string{"#00ff00", "#ff0000", "#0000ff", "#000000"} - -type V13 struct { - API string `json:"api"` - Cache struct { - Schedule string `json:"schedule"` - } `json:"cache"` - Cam []string `json:"cam"` - Contact struct { - Email string `json:"email"` - Facebook string `json:"facebook"` - Foursquare string `json:"foursquare"` - Google struct { - Plus string `json:"plus"` - } `json:"google"` - Identica string `json:"identica"` - Irc string `json:"irc"` - IssueMail string `json:"issue_mail"` - Jabber string `json:"jabber"` - Keymasters []struct { - Email string `json:"email"` - IrcNick string `json:"irc_nick"` - Name string `json:"name"` - Phone string `json:"phone"` - Twitter string `json:"twitter"` - } `json:"keymasters"` - Ml string `json:"ml"` - Phone string `json:"phone"` - Sip string `json:"sip"` - Twitter string `json:"twitter"` - } `json:"contact"` - Events []struct { - Extra string `json:"extra"` - Name string `json:"name"` - Timestamp float64 `json:"timestamp"` - Type string `json:"type"` - } `json:"events"` - Feeds struct { - Blog struct { - Type string `json:"type"` - URL string `json:"url"` - } `json:"blog"` - Calendar struct { - Type string `json:"type"` - URL string `json:"url"` - } `json:"calendar"` - Flickr struct { - Type string `json:"type"` - URL string `json:"url"` - } `json:"flickr"` - Wiki struct { - Type string `json:"type"` - URL string `json:"url"` - } `json:"wiki"` - } `json:"feeds"` - IssueReportChannels []string `json:"issue_report_channels"` - Location struct { - Address string `json:"address"` - Lat float64 `json:"lat"` - Lon float64 `json:"lon"` - } `json:"location"` - Logo string `json:"logo"` - Projects []string `json:"projects"` - RadioShow []struct { - End string `json:"end"` - Name string `json:"name"` - Start string `json:"start"` - Type string `json:"type"` - URL string `json:"url"` - } `json:"radio_show"` - Sensors struct { - AccountBalance []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"account_balance"` - Barometer []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"barometer"` - BeverageSupply []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"beverage_supply"` - DoorLocked []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Value bool `json:"value"` - } `json:"door_locked"` - Humidity []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"humidity"` - NetworkConnections []struct { - Description string `json:"description"` - Location string `json:"location"` - Machines []struct { - Mac string `json:"mac"` - Name string `json:"name"` - } `json:"machines"` - Name string `json:"name"` - Type string `json:"type"` - Value float64 `json:"value"` - } `json:"network_connections"` - PeopleNowPresent []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Names []string `json:"names"` - Value float64 `json:"value"` - } `json:"people_now_present"` - PowerConsumption []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"power_consumption"` - Radiation struct { - Alpha []struct { - ConversionFactor float64 `json:"conversion_factor"` - DeadTime float64 `json:"dead_time"` - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"alpha"` - Beta []struct { - ConversionFactor float64 `json:"conversion_factor"` - DeadTime float64 `json:"dead_time"` - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"beta"` - BetaGamma []struct { - ConversionFactor float64 `json:"conversion_factor"` - DeadTime float64 `json:"dead_time"` - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"beta_gamma"` - Gamma []struct { - ConversionFactor float64 `json:"conversion_factor"` - DeadTime float64 `json:"dead_time"` - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"gamma"` - } `json:"radiation"` - Temperature []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"temperature"` - TotalMemberCount []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Value float64 `json:"value"` - } `json:"total_member_count"` - Wind []struct { - Description string `json:"description"` - Location string `json:"location"` - Name string `json:"name"` - Properties struct { - Direction struct { - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"direction"` - Elevation struct { - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"elevation"` - Gust struct { - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"gust"` - Speed struct { - Unit string `json:"unit"` - Value float64 `json:"value"` - } `json:"speed"` - } `json:"properties"` - } `json:"wind"` - } `json:"sensors"` - Space string `json:"space"` - Spacefed struct { - Spacenet bool `json:"spacenet"` - Spacephone bool `json:"spacephone"` - Spacesaml bool `json:"spacesaml"` - } `json:"spacefed"` - State struct { - Icon struct { - Closed string `json:"closed"` - Open string `json:"open"` - } `json:"icon"` - Lastchange float64 `json:"lastchange"` - Message string `json:"message"` - Open interface{} `json:"open"` - TriggerPerson string `json:"trigger_person"` - } `json:"state"` - Stream struct { - M4 string `json:"m4"` - Mjpeg string `json:"mjpeg"` - Ustream string `json:"ustream"` - } `json:"stream"` - URL string `json:"url"` -} \ No newline at end of file