From 8ba19d05023bfca682723d1d4e080c95a0c0b351 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Dec 2024 18:55:26 +0100 Subject: [PATCH 001/102] Adds a minimal example system and closes #14 --- ethermostat/main.go | 100 ++++++++++++++++++++++++++++++++++++++++++ ethermostat/notes.md | 19 ++++++++ ethermostat/thing.go | 101 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 + 5 files changed, 224 insertions(+) create mode 100644 ethermostat/main.go create mode 100644 ethermostat/notes.md create mode 100644 ethermostat/thing.go diff --git a/ethermostat/main.go b/ethermostat/main.go new file mode 100644 index 0000000..7232467 --- /dev/null +++ b/ethermostat/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" +) + +func main() { + // Handle graceful shutdowns using this context. It should always be canceled, + // no matter the final execution path so all computer resources are freed up. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Create a new Eclipse Arrowhead application system and then wrap it with a + // "husk" (aka a wrapper or shell), which then sets up various properties and + // operations that's required of an Arrowhead system. + sys := components.NewSystem("ethermostat", ctx) + sys.Husk = &components.Husk{ + Description: "reads the temperature from sensors", + Details: map[string][]string{"Developer": {"Group10"}}, + ProtoPort: map[string]int{"https": 8691, "http": 8690, "coap": 0}, + InfoLink: "https://github.com/lmas/d0020e_code/tree/master/ethermostat", + } + + // Try loading the config file (in JSON format) for this deployment, + // by using a unit asset with default values. + uat := initTemplate() + sys.UAssets[uat.GetName()] = &uat + rawUAs, servsTemp, err := usecases.Configure(&sys) + // If the file is missing, a new config will be created and an error is returned here. + if err != nil { + log.Fatalf("Configuration error: %v\n", err) + } + + // Load the proper unit asset(s) using the user-defined settings from the config file. + // sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + clear(sys.UAssets) + for _, raw := range rawUAs { + var uac UnitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("UnitAsset configuration error: %+v\n", err) + } + ua, cleanup := newUnitAsset(uac, &sys, servsTemp) + sys.UAssets[ua.GetName()] = &ua + defer cleanup() + } + + // Generate PKI keys and CSR to obtain a authentication certificate from the CA + usecases.RequestCertificate(&sys) + + // Register the (system) and its services + usecases.RegisterServices(&sys) + + // start the requests handlers and servers + go usecases.SetoutServers(&sys) + + // Wait for the shutdown signal (ctrl+c) and gracefully terminate any goroutines by cancelling the context. + <-sys.Sigs + log.Println("Shuting down system: " + sys.Name) + cancel() + + // Allow goroutines to finish execution (might take more time than main to end) + time.Sleep(2 * time.Second) +} + +//////////////////////////////////////////////////////////////////////////////// + +// Serving maps the requested service paths with any request handlers. +func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + switch servicePath { + // TODO: match this subpath in a better way with the subpath defined in thing.go, ie. without relying on magic values + case "temperature-sub": + ua.getTemp(w, r) + default: + http.Error(w, "Invalid service request", http.StatusBadRequest) + } +} + +// getTemp returns the temperature of this sensor, using an analog signal form. +func (ua *UnitAsset) getTemp(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Method is not supported.", http.StatusNotFound) + return + } + + // Create and fill out the return form + var f forms.SignalA_v1a + f.NewForm() + f.Value = ua.temperature + f.Unit = "Celsius" + f.Timestamp = time.Now() + usecases.HTTPProcessGetRequest(w, r, &f) +} diff --git a/ethermostat/notes.md b/ethermostat/notes.md new file mode 100644 index 0000000..6f27f2e --- /dev/null +++ b/ethermostat/notes.md @@ -0,0 +1,19 @@ + +# Work order + +Create thing.go: + +- setup the unit asset +- add required methods to meet interface +- add constructor for making default unit asset +- add constructor for creating unit asset based on config + +Create main.go: + +- create new system and associated husk +- create default unit asset +- try loading config for system +- load individual unit assets and aossciate them with the system +- generate certs and register system +- run web servers and wait for shutdown +- add web handlers diff --git a/ethermostat/thing.go b/ethermostat/thing.go new file mode 100644 index 0000000..f0f1dfe --- /dev/null +++ b/ethermostat/thing.go @@ -0,0 +1,101 @@ +package main + +import ( + "log" + + "github.com/sdoque/mbaigo/components" +) + +// A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. +// This type must implement the go interface of "components.UnitAsset" +type UnitAsset struct { + // Public fields + // TODO: Why have these public and then provide getter methods? Might need refactor.. + Name string `json:"name"` // Must be a unique name, ie. a sensor ID + Owner *components.System `json:"-"` // The parent system this UA is part of + Details map[string][]string `json:"details"` // Metadata or details about this UA + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` + + // Internal fields this UA might need to perform it's function, example: + temperature float64 +} + +func (ua *UnitAsset) GetName() string { + return ua.Name +} + +func (ua *UnitAsset) GetDetails() map[string][]string { + return ua.Details +} + +// GetServices returns all services and capabilities this UnitAsset is providing to consumers. +func (ua *UnitAsset) GetServices() components.Services { + return ua.ServicesMap +} + +// GetCervices returns the list of services that is being consumed by this UnitAsset. +func (ua *UnitAsset) GetCervices() components.Cervices { + return ua.CervicesMap +} + +// ensure UnitAsset implements the components.UnitAsset interface (this check is done at compile time) +var _ components.UnitAsset = (*UnitAsset)(nil) + +//////////////////////////////////////////////////////////////////////////////// + +// initTemplate initializes a new UA and prefils it with some default values. +// The returned instance is used for generating the configuration file, whenever it's missing. +func initTemplate() components.UnitAsset { + // First predefine any exposed services + // (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) + temperature := components.Service{ + Definition: "temperature-def", // TODO: this get's incorrectly linked to the below subpath + SubPath: "temperature-sub", // TODO: this path needs to be setup in Serving() too + Details: map[string][]string{"Forms": {"SignalA_v1a"}}, // TODO: why this form here?? + RegPeriod: 30, + Description: "provides the current temperature of this sensor (using a GET request)", + } + + return &UnitAsset{ + // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) + Name: "temperature-UA", + Details: map[string][]string{ + "Unit": {"Celsius"}, + "Location": {"Kitchen"}, + }, + // Don't forget to map the provided services from above! + ServicesMap: components.Services{ + temperature.SubPath: &temperature, + }, + } +} + +//////////////////////////////////////////////////////////////////////////////// + +// newUnitAsset creates a new and proper instance of UnitAsset, using settings and +// values loaded from an existing configuration file. +// This function returns an UA instance that is ready to be published and used, +// aswell as a function that can perform any cleanup when the system is shutting down. +func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { + ua := &UnitAsset{ + // Filling in public fields using the given data + Name: uac.Name, + Owner: sys, + Details: uac.Details, + ServicesMap: components.CloneServices(servs), + + // Setting the example variable + temperature: 3.14, + } + + // Optionally start background tasks here! Example: + go func() { + log.Println("Starting up " + ua.Name) + }() + + // Returns the loaded unit asset and an function to handle optional cleanup at shutdown + return ua, func() { + log.Println("Cleaning up " + ua.Name) + } +} diff --git a/go.mod b/go.mod index d522536..154fd54 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/lmas/d0020e_code go 1.22.6 + +require github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 diff --git a/go.sum b/go.sum index e69de29..0f4b1d6 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 h1:feRW3hSquROFeId8H0ZEUsH/kEzd4AAVxjsYkQd1cCs= +github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4/go.mod h1:Bfx9Uj0uiTT7BCzzlImMiRd6vMoPQdsZIHGMQOVjx80= From 16bc5cc5889863556c0f4c242a0ae73bc22b4010 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 Dec 2024 15:53:18 +0100 Subject: [PATCH 002/102] minor cleanup --- ethermostat/main.go | 4 +++- ethermostat/thing.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ethermostat/main.go b/ethermostat/main.go index 7232467..6568064 100644 --- a/ethermostat/main.go +++ b/ethermostat/main.go @@ -1,5 +1,8 @@ package main +// This file was originally copied from: +// https://github.com/sdoque/systems/blob/main/ds18b20/ds18b20.go + import ( "context" "encoding/json" @@ -40,7 +43,6 @@ func main() { } // Load the proper unit asset(s) using the user-defined settings from the config file. - // sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) clear(sys.UAssets) for _, raw := range rawUAs { var uac UnitAsset diff --git a/ethermostat/thing.go b/ethermostat/thing.go index f0f1dfe..ecae759 100644 --- a/ethermostat/thing.go +++ b/ethermostat/thing.go @@ -1,5 +1,8 @@ package main +// This file was originally copied from: +// https://github.com/sdoque/systems/blob/main/ds18b20/thing.go + import ( "log" From 2e3dbe9e6f718989723a7137e8fd92b0e965739e Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 24 Jan 2025 09:35:13 +0100 Subject: [PATCH 003/102] added return statements to errors --- ZigBeeValve/thing.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index d62ef63..c7e88d5 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -198,19 +198,23 @@ func findGateway(ua *UnitAsset) { res, err := http.Get("https://phoscon.de/discover") if err != nil { log.Println("Couldn't get gateway, error:", err) + return } defer res.Body.Close() if res.StatusCode > 299 { log.Printf("Response failed with status code: %d and\n", res.StatusCode) + return } body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { log.Println("Something went wrong while reading the body during discovery, error:", err) + return } var gw []discoverJSON // Create a list to hold the gateway json err = json.Unmarshal(body, &gw) // "unpack" body from []byte to []discoverJSON, save errors if err != nil { log.Println("Error during Unmarshal, error:", err) + return } if len(gw) < 1 { log.Println("No gateway was found") @@ -282,8 +286,10 @@ func sendRequest(data []byte, apiURL string) { b, err := io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { log.Println("Something went wrong while reading the body during discovery, error:", err) + return } if resp.StatusCode > 299 { log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) + return } } From 5aea0b999a62785611eba3132fcd643a0a440137 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 24 Jan 2025 09:38:25 +0100 Subject: [PATCH 004/102] Added testfile --- ZigBeeValve/zigbee_test.go | 100 +++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 ZigBeeValve/zigbee_test.go diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go new file mode 100644 index 0000000..0706247 --- /dev/null +++ b/ZigBeeValve/zigbee_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/sdoque/mbaigo/forms" +) + +// mockTransport is used for replacing the default network Transport (used by +// http.DefaultClient) and it will intercept network requests. + +type mockTransport struct { + hits map[string]int +} + +func newMockTransport() mockTransport { + t := mockTransport{ + hits: make(map[string]int), + } + // Highjack the default http client so no actuall http requests are sent over the network + http.DefaultClient.Transport = t + return t +} + +// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). + +func (t mockTransport) domainHits(domain string) int { + for u, hits := range t.hits { + if u == domain { + return hits + } + } + return -1 +} + +// TODO: this might need to be expanded to a full JSON array? + +const priceExample string = `[{ + "SEK_per_kWh": 0.26673, + "EUR_per_kWh": 0.02328, + "EXR": 11.457574, + "time_start": "2025-01-06T%02d:00:00+01:00", + "time_end": "2025-01-06T%02d:00:00+01:00" +}]` + +// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). +// It prevents the request from being sent over the network and count how many times +// a domain was requested. + +func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + hour := time.Now().Local().Hour() + fakeBody := fmt.Sprintf(priceExample, hour, hour+1) + // TODO: should be able to adjust these return values for the error cases + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Request: req, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + t.hits[req.URL.Hostname()] += 1 + return +} + +//////////////////////////////////////////////////////////////////////////////// + +const thermostatDomain string = "http://localhost:port/api/apikey/sensors/thermostat_index/config" +const plugDomain string = "http://localhost:port/api/apikey/lights/plug_index/config" + +func TestUnitAssetChanged(t *testing.T) { + + // Don't understand how to check my own deConz API calls, will extend the test with this once i understand + trans := newMockTransport() + + // Create a form + f := forms.SignalA_v1a{ + Value: 27.0, + } + + // Creates a single UnitAsset and assert it changes + ua := UnitAsset{ + Setpt: 20.0, + } + + ua.setSetPoint(f) + + if ua.Setpt != 27.0 { + t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) + } + + // TODO: Add api call to make sure it only sends update to HW once. + hits := trans.domainHits(thermostatDomain) + if hits > 1 { + t.Errorf("Expected number of api requests = 1, got %d requests", hits) + } +} From 8800a7e0fb63b6f92b6a1d2e9357f169d924e950 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 11:21:21 +0100 Subject: [PATCH 005/102] Installs dependencies for the tests too in the workflow --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d0f615..d95f148 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.23 + - name: Install dependencies + run: make deps - name: Run tests run: make test - name: Report stats From c45cedfad93e06dfd1f3a135188e88d2a2641c82 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 24 Jan 2025 15:25:39 +0100 Subject: [PATCH 006/102] added some tests --- ZigBeeValve/thing.go | 2 +- ZigBeeValve/zigbee_test.go | 59 ++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index c7e88d5..1f352fe 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -82,7 +82,7 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "2", + Name: "Template", Details: map[string][]string{"Location": {"Kitchen"}}, Model: "", Period: 10, diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 0706247..009246d 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "net/http" @@ -8,6 +9,7 @@ import ( "testing" "time" + "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" ) @@ -68,10 +70,10 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er //////////////////////////////////////////////////////////////////////////////// -const thermostatDomain string = "http://localhost:port/api/apikey/sensors/thermostat_index/config" -const plugDomain string = "http://localhost:port/api/apikey/lights/plug_index/config" +const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" +const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" -func TestUnitAssetChanged(t *testing.T) { +func TestUnitAsset(t *testing.T) { // Don't understand how to check my own deConz API calls, will extend the test with this once i understand trans := newMockTransport() @@ -82,10 +84,9 @@ func TestUnitAssetChanged(t *testing.T) { } // Creates a single UnitAsset and assert it changes - ua := UnitAsset{ - Setpt: 20.0, - } + ua := initTemplate().(*UnitAsset) + // Change Setpt ua.setSetPoint(f) if ua.Setpt != 27.0 { @@ -98,3 +99,49 @@ func TestUnitAssetChanged(t *testing.T) { t.Errorf("Expected number of api requests = 1, got %d requests", hits) } } + +func TestGetters(t *testing.T) { + ua := initTemplate().(*UnitAsset) + + name := ua.GetName() + if name != "Template" { + t.Errorf("Expected name to be 2, instead got %s", name) + } + + services := ua.GetServices() + if services == nil { + t.Fatalf("Expected services not to be nil") + } + if services["setpoint"].Definition != "setpoint" { + t.Errorf("Expected definition to be setpoint") + } + + details := ua.GetDetails() + if details == nil { + t.Fatalf("Details was nil, expected map") + } + if len(details["Location"]) == 0 { + t.Fatalf("Location was nil, expected kitchen") + } + + if details["Location"][0] != "Kitchen" { + t.Errorf("Expected location to be Kitchen") + } + + cervices := ua.GetCervices() + if cervices != nil { + t.Errorf("Expected no cervices") + } +} + +func TestNewResource(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var uac UnitAsset + sys := components.NewSystem("testsys", ctx) + servsTemp := []components.Service{} + + ua, cleanup := newResource(uac, &sys, servsTemp) + +} From e0cf7f8432620567cba3ce179337360dfeff784d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 16:39:36 +0100 Subject: [PATCH 007/102] Removes unrelated systems --- Comfortstat/Comfortstat.go | 184 -------------- Comfortstat/api_fetch_test.go | 105 -------- Comfortstat/things.go | 458 ---------------------------------- ZigBeeValve/ZigBeeValve.go | 103 -------- ZigBeeValve/thing.go | 289 --------------------- 5 files changed, 1139 deletions(-) delete mode 100644 Comfortstat/Comfortstat.go delete mode 100644 Comfortstat/api_fetch_test.go delete mode 100644 Comfortstat/things.go delete mode 100644 ZigBeeValve/ZigBeeValve.go delete mode 100644 ZigBeeValve/thing.go diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go deleted file mode 100644 index cd2480c..0000000 --- a/Comfortstat/Comfortstat.go +++ /dev/null @@ -1,184 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/usecases" -) - -func main() { - // prepare for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled - defer cancel() // make sure all paths cancel the context to avoid context leak - - // instantiate the System - sys := components.NewSystem("Comfortstat", ctx) - - // Instatiate the Capusle - sys.Husk = &components.Husk{ - Description: " is a controller for a consumed servo motor position based on a consumed temperature", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, - InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", - } - - // instantiate a template unit asset - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate - - // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, cleanup := newUnitAsset(uac, &sys, servsTemp) - defer cleanup() - sys.UAssets[ua.GetName()] = &ua - } - - // Generate PKI keys and CSR to obtain a authentication certificate from the CA - usecases.RequestCertificate(&sys) - - // Register the (system) and its services - usecases.RegisterServices(&sys) - - // start the http handler and server - go usecases.SetoutServers(&sys) - - // wait for shutdown signal, and gracefully close properly goroutines with context - <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - fmt.Println("\nshuting down system", sys.Name) - cancel() // cancel the context, signaling the goroutines to stop - time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end -} - -// TODO: change the namne, will get one function for each of the four cases -// Serving handles the resources services. NOTE: it exepcts those names from the request URL path -func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { - switch servicePath { - case "min_temperature": - t.set_minTemp(w, r) - case "max_temperature": - t.set_maxTemp(w, r) - case "max_price": - t.set_maxPrice(w, r) - case "min_price": - t.set_minPrice(w, r) - case "SEK_price": - t.set_SEKprice(w, r) - case "desired_temp": - t.set_desiredTemp(w, r) - default: - http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) - } -} - -func (rsc *UnitAsset) set_SEKprice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - signalErr := rsc.getSEK_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} - -// TODO: split up this function to two sepreate function that sets on max and min price. -func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMin_temp(sig) - case "GET": - signalErr := rsc.getMin_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} -func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMax_temp(sig) - case "GET": - signalErr := rsc.getMax_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} - -// LOOK AT: I guess that we probable only need to if there is a PUT from user? -// LOOK AT: so not the GET! -// For PUT - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, call set_minMaxprice with the value -// wich updates the value in thge struct -func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMin_price(sig) - case "GET": - signalErr := rsc.getMin_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - - } -} - -func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMax_price(sig) - case "GET": - signalErr := rsc.getMax_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - - } -} - -func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setDesired_temp(sig) - case "GET": - signalErr := rsc.getDesired_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go deleted file mode 100644 index 238e1d9..0000000 --- a/Comfortstat/api_fetch_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "strings" - "testing" - "time" -) - -// mockTransport is used for replacing the default network Transport (used by -// http.DefaultClient) and it will intercept network requests. -type mockTransport struct { - hits map[string]int -} - -func newMockTransport() mockTransport { - t := mockTransport{ - hits: make(map[string]int), - } - // Highjack the default http client so no actuall http requests are sent over the network - http.DefaultClient.Transport = t - return t -} - -// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). -func (t mockTransport) domainHits(domain string) int { - for u, hits := range t.hits { - if u == domain { - return hits - } - } - return -1 -} - -// TODO: this might need to be expanded to a full JSON array? -const priceExample string = `[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" -}]` - -// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). -// It prevents the request from being sent over the network and count how many times -// a domain was requested. -func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) - // TODO: should be able to adjust these return values for the error cases - resp = &http.Response{ - Status: "200 OK", - StatusCode: 200, - Request: req, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - t.hits[req.URL.Hostname()] += 1 - return -} - -//////////////////////////////////////////////////////////////////////////////// - -const apiDomain string = "www.elprisetjustnu.se" - -func TestAPIDataFetchPeriod(t *testing.T) { - want := 3600 - if apiFetchPeriod < want { - t.Errorf("expected API fetch period >= %d, got %d", want, apiFetchPeriod) - } -} - -func TestSingleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates a single UnitAsset and assert it only sends a single API request - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - - // TEST CASE: cause a single API request - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests", hits) - } - - // TODO: try more test cases! -} - -func TestMultipleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates multiple UnitAssets and monitor their API requests - units := 10 - for i := 0; i < units; i++ { - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - } - - // TEST CASE: causing only one API hit while using multiple UnitAssets - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests (from %d units)", hits, units) - } - - // TODO: more test cases?? -} diff --git a/Comfortstat/things.go b/Comfortstat/things.go deleted file mode 100644 index 711e710..0000000 --- a/Comfortstat/things.go +++ /dev/null @@ -1,458 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log" - "math" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" -) - -// A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. -// This type must implement the go interface of "components.UnitAsset" -type UnitAsset struct { - // Public fields - // TODO: Why have these public and then provide getter methods? Might need refactor.. - Name string `json:"name"` // Must be a unique name, ie. a sensor ID - Owner *components.System `json:"-"` // The parent system this UA is part of - Details map[string][]string `json:"details"` // Metadata or details about this UA - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` - // - Period time.Duration `json:"samplingPeriod"` - // - Daily_prices []API_data `json:"-"` - Desired_temp float64 `json:"desired_temp"` - old_desired_temp float64 // keep this field private! - SEK_price float64 `json:"SEK_per_kWh"` - Min_price float64 `json:"min_price"` - Max_price float64 `json:"max_price"` - Min_temp float64 `json:"min_temp"` - Max_temp float64 `json:"max_temp"` -} - -type API_data struct { - SEK_price float64 `json:"SEK_per_kWh"` - EUR_price float64 `json:"EUR_per_kWh"` - EXR float64 `json:"EXR"` - Time_start string `json:"time_start"` - Time_end string `json:"time_end"` -} - -// GetName returns the name of the Resource. -func (ua *UnitAsset) GetName() string { - return ua.Name -} - -// GetServices returns the services of the Resource. -func (ua *UnitAsset) GetServices() components.Services { - return ua.ServicesMap -} - -// GetCervices returns the list of consumed services by the Resource. -func (ua *UnitAsset) GetCervices() components.Cervices { - return ua.CervicesMap -} - -// GetDetails returns the details of the Resource. -func (ua *UnitAsset) GetDetails() map[string][]string { - return ua.Details -} - -// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) -var _ components.UnitAsset = (*UnitAsset)(nil) - -//////////////////////////////////////////////////////////////////////////////// - -// initTemplate initializes a new UA and prefils it with some default values. -// The returned instance is used for generating the configuration file, whenever it's missing. -func initTemplate() components.UnitAsset { - // First predefine any exposed services - // (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) - setSEK_price := components.Service{ - Definition: "SEK_price", - SubPath: "SEK_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the current electric hourly price (using a GET request)", - } - - setMax_temp := components.Service{ - Definition: "max_temperature", // TODO: this get's incorrectly linked to the below subpath - SubPath: "max_temperature", // TODO: this path needs to be setup in Serving() too - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, // TODO: why this form here?? - Description: "provides the maximum temp the user wants (using a GET request)", - } - setMin_temp := components.Service{ - Definition: "min_temperature", - SubPath: "min_temperature", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum temp the user could tolerate (using a GET request)", - } - setMax_price := components.Service{ - Definition: "max_price", - SubPath: "max_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the maximum price the user wants to pay (using a GET request)", - } - setMin_price := components.Service{ - Definition: "min_price", - SubPath: "min_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum price the user wants to pay (using a GET request)", - } - setDesired_temp := components.Service{ - Definition: "desired_temp", - SubPath: "desired_temp", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", - } - - return &UnitAsset{ - // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", - Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 7.5, // Example electricity price in SEK per kWh - Min_price: 0.0, // Minimum price allowed - Max_price: 0.02, // Maximum price allowed - Min_temp: 20.0, // Minimum temperature - Max_temp: 25.0, // Maximum temprature allowed - Desired_temp: 0, // Desired temp calculated by system - Period: 15, - - // Don't forget to map the provided services from above! - ServicesMap: components.Services{ - setMax_temp.SubPath: &setMax_temp, - setMin_temp.SubPath: &setMin_temp, - setMax_price.SubPath: &setMax_price, - setMin_price.SubPath: &setMin_price, - setSEK_price.SubPath: &setSEK_price, - setDesired_temp.SubPath: &setDesired_temp, - }, - } -} - -//////////////////////////////////////////////////////////////////////////////// - -// newUnitAsset creates a new and proper instance of UnitAsset, using settings and -// values loaded from an existing configuration file. -// This function returns an UA instance that is ready to be published and used, -// aswell as a function that can perform any cleanup when the system is shutting down. -func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { - - sProtocol := components.SProtocols(sys.Husk.ProtoPort) - - // the Cervice that is to be consumed by zigbee, there fore the name with the C - - t := &components.Cervice{ - Name: "setpoint", - Protos: sProtocol, - Url: make([]string, 0), - } - - ua := &UnitAsset{ - // Filling in public fields using the given data - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - SEK_price: uac.SEK_price, - Min_price: uac.Min_price, - Max_price: uac.Max_price, - Min_temp: uac.Min_temp, - Max_temp: uac.Max_temp, - Desired_temp: uac.Desired_temp, - Period: uac.Period, - CervicesMap: components.Cervices{ - t.Name: t, - }, - } - - var ref components.Service - for _, s := range servs { - if s.Definition == "desired_temp" { - ref = s - } - } - - ua.CervicesMap["setpoint"].Details = components.MergeDetails(ua.Details, ref.Details) - - // ua.CervicesMap["setPoint"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}) - - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - go ua.API_feedbackLoop(sys.Ctx) - - // Optionally start background tasks here! Example: - go func() { - log.Println("Starting up " + ua.Name) - }() - - // Returns the loaded unit asset and an function to handle optional cleanup at shutdown - return ua, func() { - log.Println("Cleaning up " + ua.Name) - } -} - -// getSEK_price is used for reading the current hourly electric price -func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.SEK_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setSEK_price updates the current electric price with the new current electric hourly price -func (ua *UnitAsset) setSEK_price(f forms.SignalA_v1a) { - ua.SEK_price = f.Value - log.Printf("new electric price: %.1f", f.Value) -} - -///////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////// - -// getMin_price is used for reading the current value of Min_price -func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Min_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setMin_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { - ua.Min_price = f.Value - log.Printf("new minimum price: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMax_price is used for reading the current value of Max_price -func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Max_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setMax_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { - ua.Max_price = f.Value - log.Printf("new maximum price: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMin_temp is used for reading the current minimum temerature value -func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Min_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -// setMin_temp updates the current minimum temperature set by the user with a new value -func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { - ua.Min_temp = f.Value - log.Printf("new minimum temperature: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMax_temp is used for reading the current value of Min_price -func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Max_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -// setMax_temp updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { - ua.Max_temp = f.Value - log.Printf("new maximum temperature: %.1f", f.Value) - ua.processFeedbackLoop() -} - -func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Desired_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -func (ua *UnitAsset) setDesired_temp(f forms.SignalA_v1a) { - ua.Desired_temp = f.Value - log.Printf("new desired temperature: %.1f", f.Value) -} - -//TODO: This fuction is used for checking the electric price ones every x hours and so on -//TODO: Needs to be modified to match our needs, not using processFeedbacklopp -//TODO: So mayby the period is every hour, call the api to receive the current price ( could be every 24 hours) -//TODO: This function is may be better in the COMFORTSTAT MAIN - -// It's _strongly_ encouraged to not send requests to the API for more than once per hour. -// Making this period a private constant prevents a user from changing this value -// in the config file. -const apiFetchPeriod int = 3600 - -// feedbackLoop is THE control loop (IPR of the system) -func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) - defer ticker.Stop() - - // start the control loop - for { - retrieveAPI_price(ua) - select { - case <-ticker.C: - // Block the loop until the next period - case <-ctx.Done(): - return - } - } -} - -func retrieveAPI_price(ua *UnitAsset) { - url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) - log.Println("URL:", url) - - res, err := http.Get(url) - if err != nil { - log.Println("Couldn't get the url, error:", err) - return - } - body, err := io.ReadAll(res.Body) // Read the payload into body variable - if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) - return - } - var data []API_data // Create a list to hold the gateway json - err = json.Unmarshal(body, &data) // "unpack" body from []byte to []discoverJSON, save errors - res.Body.Close() // defer res.Body.Close() - - if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) - return - } - if err != nil { - log.Println("Error during Unmarshal, error:", err) - return - } - - ///////// - now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) - for _, i := range data { - if i.Time_start == now { - ua.SEK_price = i.SEK_price - log.Println("Price in loop is:", i.SEK_price) - } - - } - log.Println("current el-pris is:", ua.SEK_price) - - // Don't send temperature updates if the difference is too low - // (this could potentially save on battery!) - new_temp := ua.calculateDesiredTemp() - if math.Abs(ua.Desired_temp-new_temp) < 0.5 { - return - } - ua.Desired_temp = new_temp -} - -// feedbackLoop is THE control loop (IPR of the system) -func (ua *UnitAsset) feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(ua.Period * time.Second) - defer ticker.Stop() - - // start the control loop - for { - select { - case <-ticker.C: - ua.processFeedbackLoop() // either modifiy processFeedback loop or write a new one - case <-ctx.Done(): - return - } - } -} - -// - -func (ua *UnitAsset) processFeedbackLoop() { - // get the current temperature - /* - tf, err := usecases.GetState(ua.CervicesMap["setpoint"], ua.Owner) - if err != nil { - log.Printf("\n unable to obtain a setpoint reading error: %s\n", err) - return - } - // Perform a type assertion to convert the returned Form to SignalA_v1a - tup, ok := tf.(*forms.SignalA_v1a) - if !ok { - log.Println("problem unpacking the setpoint signal form") - return - } - */ - /* - miT := ua.getMin_temp().Value - maT := ua.getMax_temp().Value - miP := ua.getMin_price().Value - maP := ua.getMax_price().Value - */ - //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) - // Only send temperature update when we have a new value. - if ua.Desired_temp == ua.old_desired_temp { - return - } - // Keep track of previous value - ua.old_desired_temp = ua.Desired_temp - - // perform the control algorithm - // ua.deviation = ua.Setpt - tup.Value - // output := ua.calculateOutput(ua.deviation) - - // prepare the form to send - var of forms.SignalA_v1a - of.NewForm() - of.Value = ua.Desired_temp - of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] - of.Timestamp = time.Now() - - // pack the new valve state form - op, err := usecases.Pack(&of, "application/json") - if err != nil { - return - } - // send the new valve state request - err = usecases.SetState(ua.CervicesMap["setpoint"], ua.Owner, op) - if err != nil { - log.Printf("cannot update zigbee setpoint: %s\n", err) - return - } -} - -func (ua *UnitAsset) calculateDesiredTemp() float64 { - if ua.SEK_price <= ua.Min_price { - return ua.Max_temp - } - if ua.SEK_price >= ua.Max_price { - return ua.Min_temp - } - - k := -(ua.Max_temp - ua.Min_temp) / (ua.Max_price - ua.Min_price) - //m := max_temp - (k * min_price) - //m := max_temp - desired_temp := k*(ua.SEK_price-ua.Min_price) + ua.Min_temp // y - y_min = k*(x-x_min), solve for y ("desired temp") - return desired_temp -} diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go deleted file mode 100644 index 12d44a5..0000000 --- a/ZigBeeValve/ZigBeeValve.go +++ /dev/null @@ -1,103 +0,0 @@ -/* In order to follow the structure of the other systems made before this one, most functions and structs are copied and slightly edited from: - * https://github.com/sdoque/systems/blob/main/thermostat/thermostat.go */ - -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/usecases" -) - -func main() { - // prepare for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled - defer cancel() // make sure all paths cancel the context to avoid context leak - - // instantiate the System - sys := components.NewSystem("ZigBee", ctx) - - // Instatiate the Capusle - sys.Husk = &components.Husk{ - Description: " is a controller for smart thermostats connected with a RaspBee II", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, - InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", - } - - // instantiate a template unit asset - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate - - // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, cleanup := newResource(uac, &sys, servsTemp) - defer cleanup() - sys.UAssets[ua.GetName()] = &ua - } - - // Generate PKI keys and CSR to obtain a authentication certificate from the CA - usecases.RequestCertificate(&sys) - - // Register the (system) and its services - usecases.RegisterServices(&sys) - - // start the http handler and server - go usecases.SetoutServers(&sys) - - // wait for shutdown signal, and gracefully close properly goroutines with context - <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - fmt.Println("\nshuting down system", sys.Name) - cancel() // cancel the context, signaling the goroutines to stop - time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end -} - -// Serving handles the resources services. NOTE: it exepcts those names from the request URL path -func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { - switch servicePath { - case "setpoint": - t.setpt(w, r) - default: - http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) - } -} - -func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - setPointForm := rsc.getSetPoint() - usecases.HTTPProcessGetRequest(w, r, &setPointForm) - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting desired temp ", err) - } - log.Println("sig:", sig) - log.Println("URL:", r.URL) - log.Println("Model:", rsc.Model) - rsc.setSetPoint(sig) - if rsc.Model == "SmartThermostat" { - rsc.sendSetPoint() - } - - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go deleted file mode 100644 index d62ef63..0000000 --- a/ZigBeeValve/thing.go +++ /dev/null @@ -1,289 +0,0 @@ -/* In order to follow the structure of the other systems made before this one, most functions and structs are copied and slightly edited from: - * https://github.com/sdoque/systems/blob/main/thermostat/thing.go */ - -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" -) - -//------------------------------------ Used when discovering the gateway - -type discoverJSON struct { - Id string `json:"id"` - Internalipaddress string `json:"internalipaddress"` - Macaddress string `json:"macaddress"` - Internalport int `json:"internalport"` - Name string `json:"name"` - Publicipaddress string `json:"publicipaddress"` -} - -//-------------------------------------Define the unit asset - -// UnitAsset type models the unit asset (interface) of the system -type UnitAsset struct { - Name string `json:"name"` - Owner *components.System `json:"-"` - Details map[string][]string `json:"details"` - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` - // - Model string `json:"model"` - Period time.Duration `json:"period"` - Setpt float64 `json:"setpoint"` - gateway string - Apikey string `json:"APIkey"` -} - -// GetName returns the name of the Resource. -func (ua *UnitAsset) GetName() string { - return ua.Name -} - -// GetServices returns the services of the Resource. -func (ua *UnitAsset) GetServices() components.Services { - return ua.ServicesMap -} - -// GetCervices returns the list of consumed services by the Resource. -func (ua *UnitAsset) GetCervices() components.Cervices { - return ua.CervicesMap -} - -// GetDetails returns the details of the Resource. -func (ua *UnitAsset) GetDetails() map[string][]string { - return ua.Details -} - -// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) -var _ components.UnitAsset = (*UnitAsset)(nil) - -//-------------------------------------Instatiate a unit asset template - -// initTemplate initializes a UnitAsset with default values. -func initTemplate() components.UnitAsset { - setPointService := components.Service{ - Definition: "setpoint", - SubPath: "setpoint", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the current thermal setpoint (GET) or sets it (PUT)", - } - - // var uat components.UnitAsset // this is an interface, which we then initialize - uat := &UnitAsset{ - Name: "2", - Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "", - Period: 10, - Setpt: 20, - gateway: "", - Apikey: "", - ServicesMap: components.Services{ - setPointService.SubPath: &setPointService, - }, - } - return uat -} - -//-------------------------------------Instatiate the unit assets based on configuration - -// newResource creates the Resource resource with its pointers and channels based on the configuration using the tConig structs -func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { - // deterimine the protocols that the system supports - sProtocols := components.SProtocols(sys.Husk.ProtoPort) - - // instantiate the consumed services - t := &components.Cervice{ - Name: "temperature", - Protos: sProtocols, - Url: make([]string, 0), - } - - // intantiate the unit asset - ua := &UnitAsset{ - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - Model: uac.Model, - Period: uac.Period, - Setpt: uac.Setpt, - gateway: uac.gateway, - Apikey: uac.Apikey, - CervicesMap: components.Cervices{ - t.Name: t, - }, - } - - findGateway(ua) - - var ref components.Service - for _, s := range servs { - if s.Definition == "setpoint" { - ref = s - } - } - - ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) - - if uac.Model == "SmartThermostat" { - ua.sendSetPoint() - } else if uac.Model == "SmartPlug" { - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - } - - return ua, func() { - log.Println("Shutting down zigbeevalve ", ua.Name) - } -} - -func (ua *UnitAsset) feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(ua.Period * time.Second) - defer ticker.Stop() - - // start the control loop - for { - select { - case <-ticker.C: - ua.processFeedbackLoop() - case <-ctx.Done(): - return - } - } -} - -func (ua *UnitAsset) processFeedbackLoop() { - // get the current temperature - tf, err := usecases.GetState(ua.CervicesMap["temperature"], ua.Owner) - if err != nil { - log.Printf("\n unable to obtain a temperature reading error: %s\n", err) - return - } - - // Perform a type assertion to convert the returned Form to SignalA_v1a - tup, ok := tf.(*forms.SignalA_v1a) - if !ok { - log.Println("problem unpacking the temperature signal form") - return - } - - // TODO: Check diff instead of a hard over/under value? meaning it'll only turn on/off if diff is over 0.5 degrees - if tup.Value < ua.Setpt { - ua.toggleState(true) - } else { - ua.toggleState(false) - } - //log.Println("Feedback loop done.") - -} - -func findGateway(ua *UnitAsset) { - // https://pkg.go.dev/net/http#Get - // GET https://phoscon.de/discover // to find gateways, array of JSONs is returned in http body, we'll only have one so take index 0 - // GET the gateway through phoscons built in discover tool, the get will return a response, and in its body an array with JSON elements - // ours is index 0 since there's no other RaspBee/ZigBee gateways on the network - res, err := http.Get("https://phoscon.de/discover") - if err != nil { - log.Println("Couldn't get gateway, error:", err) - } - defer res.Body.Close() - if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\n", res.StatusCode) - } - body, err := io.ReadAll(res.Body) // Read the payload into body variable - if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) - } - var gw []discoverJSON // Create a list to hold the gateway json - err = json.Unmarshal(body, &gw) // "unpack" body from []byte to []discoverJSON, save errors - if err != nil { - log.Println("Error during Unmarshal, error:", err) - } - if len(gw) < 1 { - log.Println("No gateway was found") - return - } - // Save the gateway to our unitasset - s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) - ua.gateway = s - //log.Println("Gateway found:", s) -} - -//-------------------------------------Thing's resource methods - -// getSetPoint fills out a signal form with the current thermal setpoint -func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Setpt - f.Unit = "Celcius" - f.Timestamp = time.Now() - return f -} - -// setSetPoint updates the thermal setpoint -func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { - ua.Setpt = f.Value - log.Println("*---------------------*") - log.Printf("New set point: %.1f\n", f.Value) - log.Println("*---------------------*") -} - -func (ua *UnitAsset) sendSetPoint() { - // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + ua.gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" - - // Create http friendly payload - s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload - data := []byte(s) // Turned into byte array - sendRequest(data, apiURL) -} - -func (ua *UnitAsset) toggleState(state bool) { - // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + ua.gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" - - // Create http friendly payload - s := fmt.Sprintf(`{"on":%t}`, state) // Create payload - data := []byte(s) // Turned into byte array - sendRequest(data, apiURL) -} - -func sendRequest(data []byte, apiURL string) { - body := bytes.NewBuffer(data) // Put data into buffer - - req, err := http.NewRequest(http.MethodPut, apiURL, body) // Put request is made - if err != nil { - log.Println("Error making new HTTP PUT request, error:", err) - return - } - - req.Header.Set("Content-Type", "application/json") // Make sure it's JSON - - client := &http.Client{} // Make a client - resp, err := client.Do(req) // Perform the put request - if err != nil { - log.Println("Error sending HTTP PUT request, error:", err) - return - } - defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) // Read the payload into body variable - if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) - } - if resp.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) - } -} From ab51c5a2d2c355a6134da431804eb07d904dab70 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 18:17:12 +0100 Subject: [PATCH 008/102] Cleans up the example unitasset and system --- influxdb/main.go | 102 --------------------------- influxdb/notes.md | 19 ----- influxdb/system.go | 103 ++++++++++++++++++++++++++++ influxdb/{thing.go => unitasset.go} | 66 +++++++----------- 4 files changed, 126 insertions(+), 164 deletions(-) delete mode 100644 influxdb/main.go delete mode 100644 influxdb/notes.md create mode 100644 influxdb/system.go rename influxdb/{thing.go => unitasset.go} (57%) diff --git a/influxdb/main.go b/influxdb/main.go deleted file mode 100644 index 6716f04..0000000 --- a/influxdb/main.go +++ /dev/null @@ -1,102 +0,0 @@ -package main - -// This file was originally copied from: -// https://github.com/sdoque/systems/blob/main/ds18b20/ds18b20.go - -import ( - "context" - "encoding/json" - "log" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" -) - -func main() { - // Handle graceful shutdowns using this context. It should always be canceled, - // no matter the final execution path so all computer resources are freed up. - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Create a new Eclipse Arrowhead application system and then wrap it with a - // "husk" (aka a wrapper or shell), which then sets up various properties and - // operations that's required of an Arrowhead system. - sys := components.NewSystem("influxdb", ctx) - sys.Husk = &components.Husk{ - Description: "reads the temperature from sensors", - Details: map[string][]string{"Developer": {"Group10"}}, - ProtoPort: map[string]int{"https": 8691, "http": 8690, "coap": 0}, - InfoLink: "https://github.com/lmas/d0020e_code/tree/master/influxdb", - } - - // Try loading the config file (in JSON format) for this deployment, - // by using a unit asset with default values. - uat := initTemplate() - sys.UAssets[uat.GetName()] = &uat - rawUAs, servsTemp, err := usecases.Configure(&sys) - // If the file is missing, a new config will be created and an error is returned here. - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - - // Load the proper unit asset(s) using the user-defined settings from the config file. - clear(sys.UAssets) - for _, raw := range rawUAs { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("UnitAsset configuration error: %+v\n", err) - } - ua, cleanup := newUnitAsset(uac, &sys, servsTemp) - sys.UAssets[ua.GetName()] = &ua - defer cleanup() - } - - // Generate PKI keys and CSR to obtain a authentication certificate from the CA - usecases.RequestCertificate(&sys) - - // Register the (system) and its services - usecases.RegisterServices(&sys) - - // start the requests handlers and servers - go usecases.SetoutServers(&sys) - - // Wait for the shutdown signal (ctrl+c) and gracefully terminate any goroutines by cancelling the context. - <-sys.Sigs - log.Println("Shuting down system: " + sys.Name) - cancel() - - // Allow goroutines to finish execution (might take more time than main to end) - time.Sleep(2 * time.Second) -} - -//////////////////////////////////////////////////////////////////////////////// - -// Serving maps the requested service paths with any request handlers. -func (ua *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { - switch servicePath { - // TODO: match this subpath in a better way with the subpath defined in thing.go, ie. without relying on magic values - case "temperature-sub": - ua.getTemp(w, r) - default: - http.Error(w, "Invalid service request", http.StatusBadRequest) - } -} - -// getTemp returns the temperature of this sensor, using an analog signal form. -func (ua *UnitAsset) getTemp(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - http.Error(w, "Method is not supported.", http.StatusNotFound) - return - } - - // Create and fill out the return form - var f forms.SignalA_v1a - f.NewForm() - f.Value = ua.temperature - f.Unit = "Celsius" - f.Timestamp = time.Now() - usecases.HTTPProcessGetRequest(w, r, &f) -} diff --git a/influxdb/notes.md b/influxdb/notes.md deleted file mode 100644 index 6f27f2e..0000000 --- a/influxdb/notes.md +++ /dev/null @@ -1,19 +0,0 @@ - -# Work order - -Create thing.go: - -- setup the unit asset -- add required methods to meet interface -- add constructor for making default unit asset -- add constructor for creating unit asset based on config - -Create main.go: - -- create new system and associated husk -- create default unit asset -- try loading config for system -- load individual unit assets and aossciate them with the system -- generate certs and register system -- run web servers and wait for shutdown -- add web handlers diff --git a/influxdb/system.go b/influxdb/system.go new file mode 100644 index 0000000..c47415a --- /dev/null +++ b/influxdb/system.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "sync" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/usecases" +) + +func main() { + sys := newSystem() + sys.loadConfiguration() + sys.listenAndServe() +} + +//////////////////////////////////////////////////////////////////////////////// + +// There's no interface to use, so have to encapsulate the base struct instead +type system struct { + components.System + + cancel func() + cleanups []func() +} + +func newSystem() (sys *system) { + // Handle graceful shutdowns using this context. It should always be canceled, + // no matter the final execution path so all computer resources are freed up. + ctx, cancel := context.WithCancel(context.Background()) + + // Create a new Eclipse Arrowhead application system and then wrap it with a + // "husk" (aka a wrapper or shell), which then sets up various properties and + // operations that's required of an Arrowhead system. + // var sys system + sys = &system{ + System: components.NewSystem("influxdb", ctx), + cancel: cancel, + } + sys.Husk = &components.Husk{ + Description: "collects data from other Arrorhead systems and sends it to a InfluxDB server.", + Details: map[string][]string{"Developer": {"Alex"}}, + ProtoPort: map[string]int{"https": 8691, "http": 8690, "coap": 0}, + InfoLink: "https://github.com/lmas/d0020e_code/tree/master/influxdb", + } + return +} + +func (sys *system) loadConfiguration() { + // Try loading the config file (in JSON format) for this deployment, + // by using a unit asset with default values. + uat := initTemplate() + sys.UAssets[uat.GetName()] = &uat + rawUAs, servsTemp, err := usecases.Configure(&sys.System) + // If the file is missing, a new config will be created and an error is returned here. + if err != nil { + log.Fatalf("Error while reading configuration: %v\n", err) + } + + // Load the proper unit asset(s) using the user-defined settings from the config file. + clear(sys.UAssets) + for _, raw := range rawUAs { + var uac unitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Error while unmarshalling configuration: %+v\n", err) + } + ua, cleanup := newUnitAsset(uac, &sys.System, servsTemp) + sys.UAssets[ua.GetName()] = &ua + sys.cleanups = append(sys.cleanups, cleanup) + } +} + +func (sys *system) listenAndServe() { + var wg sync.WaitGroup // Used for counting all started goroutines + // Generate PKI keys and CSR to obtain a authentication certificate from the CA + usecases.RequestCertificate(&sys.System) + // Register the system and its services + // WARN: this func runs a goroutine of it's own, which makes it hard to count using the waitgroup (and I can't be arsed to do it properly...) + usecases.RegisterServices(&sys.System) + go func() { + wg.Add(1) + // start a web server and serve the request handlers in the unit assets + err := usecases.SetoutServers(&sys.System) + if err != nil { + log.Println("Error while running web server:", err) + sys.cancel() + } + wg.Done() + }() + + // Run any other goroutines here! + + <-sys.Sigs // Block and wait for the shutdown signal (ctrl+c) + log.Println("Shutting down system and waiting for the goroutines to terminate") + // Gracefully terminate any goroutines and wait for them to shutdown properly, before doing any cleanups + sys.cancel() + wg.Wait() + for _, f := range sys.cleanups { + f() + } +} diff --git a/influxdb/thing.go b/influxdb/unitasset.go similarity index 57% rename from influxdb/thing.go rename to influxdb/unitasset.go index ecae759..d7c4598 100644 --- a/influxdb/thing.go +++ b/influxdb/unitasset.go @@ -5,13 +5,14 @@ package main import ( "log" + "net/http" "github.com/sdoque/mbaigo/components" ) // A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. // This type must implement the go interface of "components.UnitAsset" -type UnitAsset struct { +type unitAsset struct { // Public fields // TODO: Why have these public and then provide getter methods? Might need refactor.. Name string `json:"name"` // Must be a unique name, ie. a sensor ID @@ -20,85 +21,64 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` - // Internal fields this UA might need to perform it's function, example: - temperature float64 + // Internal fields this UA might need to perform it's function } -func (ua *UnitAsset) GetName() string { +func (ua *unitAsset) GetName() string { return ua.Name } -func (ua *UnitAsset) GetDetails() map[string][]string { +func (ua *unitAsset) GetDetails() map[string][]string { return ua.Details } // GetServices returns all services and capabilities this UnitAsset is providing to consumers. -func (ua *UnitAsset) GetServices() components.Services { +func (ua *unitAsset) GetServices() components.Services { return ua.ServicesMap } // GetCervices returns the list of services that is being consumed by this UnitAsset. -func (ua *UnitAsset) GetCervices() components.Cervices { +func (ua *unitAsset) GetCervices() components.Cervices { return ua.CervicesMap } // ensure UnitAsset implements the components.UnitAsset interface (this check is done at compile time) -var _ components.UnitAsset = (*UnitAsset)(nil) +var _ components.UnitAsset = (*unitAsset)(nil) //////////////////////////////////////////////////////////////////////////////// // initTemplate initializes a new UA and prefils it with some default values. // The returned instance is used for generating the configuration file, whenever it's missing. func initTemplate() components.UnitAsset { - // First predefine any exposed services - // (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) - temperature := components.Service{ - Definition: "temperature-def", // TODO: this get's incorrectly linked to the below subpath - SubPath: "temperature-sub", // TODO: this path needs to be setup in Serving() too - Details: map[string][]string{"Forms": {"SignalA_v1a"}}, // TODO: why this form here?? - RegPeriod: 30, - Description: "provides the current temperature of this sensor (using a GET request)", - } - - return &UnitAsset{ - // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "temperature-UA", - Details: map[string][]string{ - "Unit": {"Celsius"}, - "Location": {"Kitchen"}, - }, - // Don't forget to map the provided services from above! - ServicesMap: components.Services{ - temperature.SubPath: &temperature, - }, + return &unitAsset{ + Name: "InfluxDB collector", } } -//////////////////////////////////////////////////////////////////////////////// - // newUnitAsset creates a new and proper instance of UnitAsset, using settings and // values loaded from an existing configuration file. // This function returns an UA instance that is ready to be published and used, // aswell as a function that can perform any cleanup when the system is shutting down. -func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { - ua := &UnitAsset{ - // Filling in public fields using the given data +func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { + ua := &unitAsset{ Name: uac.Name, Owner: sys, Details: uac.Details, ServicesMap: components.CloneServices(servs), - - // Setting the example variable - temperature: 3.14, } - - // Optionally start background tasks here! Example: - go func() { - log.Println("Starting up " + ua.Name) - }() - // Returns the loaded unit asset and an function to handle optional cleanup at shutdown return ua, func() { log.Println("Cleaning up " + ua.Name) } } + +//////////////////////////////////////////////////////////////////////////////// + +// Serving maps the requested service paths with any request handlers. +func (ua *unitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + switch servicePath { + default: + // TODO: should instead tell the visitor that there's no services published? + http.Error(w, "Invalid service request", http.StatusBadRequest) + } +} From 81fa3ec23121559e9475a5f8c5ece4f0ae6ad573 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 02:05:31 +0100 Subject: [PATCH 009/102] removed comfortstat from zigbee branch --- Comfortstat/Comfortstat.go | 184 -------------- Comfortstat/api_fetch_test.go | 105 -------- Comfortstat/things.go | 458 ---------------------------------- ZigBeeValve/ZigBeeValve.go | 7 +- ZigBeeValve/thing.go | 26 +- ZigBeeValve/zigbee_test.go | 32 ++- 6 files changed, 46 insertions(+), 766 deletions(-) delete mode 100644 Comfortstat/Comfortstat.go delete mode 100644 Comfortstat/api_fetch_test.go delete mode 100644 Comfortstat/things.go diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go deleted file mode 100644 index cd2480c..0000000 --- a/Comfortstat/Comfortstat.go +++ /dev/null @@ -1,184 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/usecases" -) - -func main() { - // prepare for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled - defer cancel() // make sure all paths cancel the context to avoid context leak - - // instantiate the System - sys := components.NewSystem("Comfortstat", ctx) - - // Instatiate the Capusle - sys.Husk = &components.Husk{ - Description: " is a controller for a consumed servo motor position based on a consumed temperature", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, - InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", - } - - // instantiate a template unit asset - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate - - // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, cleanup := newUnitAsset(uac, &sys, servsTemp) - defer cleanup() - sys.UAssets[ua.GetName()] = &ua - } - - // Generate PKI keys and CSR to obtain a authentication certificate from the CA - usecases.RequestCertificate(&sys) - - // Register the (system) and its services - usecases.RegisterServices(&sys) - - // start the http handler and server - go usecases.SetoutServers(&sys) - - // wait for shutdown signal, and gracefully close properly goroutines with context - <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - fmt.Println("\nshuting down system", sys.Name) - cancel() // cancel the context, signaling the goroutines to stop - time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end -} - -// TODO: change the namne, will get one function for each of the four cases -// Serving handles the resources services. NOTE: it exepcts those names from the request URL path -func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { - switch servicePath { - case "min_temperature": - t.set_minTemp(w, r) - case "max_temperature": - t.set_maxTemp(w, r) - case "max_price": - t.set_maxPrice(w, r) - case "min_price": - t.set_minPrice(w, r) - case "SEK_price": - t.set_SEKprice(w, r) - case "desired_temp": - t.set_desiredTemp(w, r) - default: - http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) - } -} - -func (rsc *UnitAsset) set_SEKprice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - signalErr := rsc.getSEK_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} - -// TODO: split up this function to two sepreate function that sets on max and min price. -func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMin_temp(sig) - case "GET": - signalErr := rsc.getMin_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} -func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMax_temp(sig) - case "GET": - signalErr := rsc.getMax_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} - -// LOOK AT: I guess that we probable only need to if there is a PUT from user? -// LOOK AT: so not the GET! -// For PUT - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, call set_minMaxprice with the value -// wich updates the value in thge struct -func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMin_price(sig) - case "GET": - signalErr := rsc.getMin_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - - } -} - -func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMax_price(sig) - case "GET": - signalErr := rsc.getMax_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - - } -} - -func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setDesired_temp(sig) - case "GET": - signalErr := rsc.getDesired_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go deleted file mode 100644 index 238e1d9..0000000 --- a/Comfortstat/api_fetch_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "strings" - "testing" - "time" -) - -// mockTransport is used for replacing the default network Transport (used by -// http.DefaultClient) and it will intercept network requests. -type mockTransport struct { - hits map[string]int -} - -func newMockTransport() mockTransport { - t := mockTransport{ - hits: make(map[string]int), - } - // Highjack the default http client so no actuall http requests are sent over the network - http.DefaultClient.Transport = t - return t -} - -// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). -func (t mockTransport) domainHits(domain string) int { - for u, hits := range t.hits { - if u == domain { - return hits - } - } - return -1 -} - -// TODO: this might need to be expanded to a full JSON array? -const priceExample string = `[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" -}]` - -// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). -// It prevents the request from being sent over the network and count how many times -// a domain was requested. -func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) - // TODO: should be able to adjust these return values for the error cases - resp = &http.Response{ - Status: "200 OK", - StatusCode: 200, - Request: req, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - t.hits[req.URL.Hostname()] += 1 - return -} - -//////////////////////////////////////////////////////////////////////////////// - -const apiDomain string = "www.elprisetjustnu.se" - -func TestAPIDataFetchPeriod(t *testing.T) { - want := 3600 - if apiFetchPeriod < want { - t.Errorf("expected API fetch period >= %d, got %d", want, apiFetchPeriod) - } -} - -func TestSingleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates a single UnitAsset and assert it only sends a single API request - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - - // TEST CASE: cause a single API request - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests", hits) - } - - // TODO: try more test cases! -} - -func TestMultipleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates multiple UnitAssets and monitor their API requests - units := 10 - for i := 0; i < units; i++ { - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - } - - // TEST CASE: causing only one API hit while using multiple UnitAssets - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests (from %d units)", hits, units) - } - - // TODO: more test cases?? -} diff --git a/Comfortstat/things.go b/Comfortstat/things.go deleted file mode 100644 index 711e710..0000000 --- a/Comfortstat/things.go +++ /dev/null @@ -1,458 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log" - "math" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" -) - -// A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. -// This type must implement the go interface of "components.UnitAsset" -type UnitAsset struct { - // Public fields - // TODO: Why have these public and then provide getter methods? Might need refactor.. - Name string `json:"name"` // Must be a unique name, ie. a sensor ID - Owner *components.System `json:"-"` // The parent system this UA is part of - Details map[string][]string `json:"details"` // Metadata or details about this UA - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` - // - Period time.Duration `json:"samplingPeriod"` - // - Daily_prices []API_data `json:"-"` - Desired_temp float64 `json:"desired_temp"` - old_desired_temp float64 // keep this field private! - SEK_price float64 `json:"SEK_per_kWh"` - Min_price float64 `json:"min_price"` - Max_price float64 `json:"max_price"` - Min_temp float64 `json:"min_temp"` - Max_temp float64 `json:"max_temp"` -} - -type API_data struct { - SEK_price float64 `json:"SEK_per_kWh"` - EUR_price float64 `json:"EUR_per_kWh"` - EXR float64 `json:"EXR"` - Time_start string `json:"time_start"` - Time_end string `json:"time_end"` -} - -// GetName returns the name of the Resource. -func (ua *UnitAsset) GetName() string { - return ua.Name -} - -// GetServices returns the services of the Resource. -func (ua *UnitAsset) GetServices() components.Services { - return ua.ServicesMap -} - -// GetCervices returns the list of consumed services by the Resource. -func (ua *UnitAsset) GetCervices() components.Cervices { - return ua.CervicesMap -} - -// GetDetails returns the details of the Resource. -func (ua *UnitAsset) GetDetails() map[string][]string { - return ua.Details -} - -// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) -var _ components.UnitAsset = (*UnitAsset)(nil) - -//////////////////////////////////////////////////////////////////////////////// - -// initTemplate initializes a new UA and prefils it with some default values. -// The returned instance is used for generating the configuration file, whenever it's missing. -func initTemplate() components.UnitAsset { - // First predefine any exposed services - // (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) - setSEK_price := components.Service{ - Definition: "SEK_price", - SubPath: "SEK_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the current electric hourly price (using a GET request)", - } - - setMax_temp := components.Service{ - Definition: "max_temperature", // TODO: this get's incorrectly linked to the below subpath - SubPath: "max_temperature", // TODO: this path needs to be setup in Serving() too - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, // TODO: why this form here?? - Description: "provides the maximum temp the user wants (using a GET request)", - } - setMin_temp := components.Service{ - Definition: "min_temperature", - SubPath: "min_temperature", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum temp the user could tolerate (using a GET request)", - } - setMax_price := components.Service{ - Definition: "max_price", - SubPath: "max_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the maximum price the user wants to pay (using a GET request)", - } - setMin_price := components.Service{ - Definition: "min_price", - SubPath: "min_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum price the user wants to pay (using a GET request)", - } - setDesired_temp := components.Service{ - Definition: "desired_temp", - SubPath: "desired_temp", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", - } - - return &UnitAsset{ - // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", - Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 7.5, // Example electricity price in SEK per kWh - Min_price: 0.0, // Minimum price allowed - Max_price: 0.02, // Maximum price allowed - Min_temp: 20.0, // Minimum temperature - Max_temp: 25.0, // Maximum temprature allowed - Desired_temp: 0, // Desired temp calculated by system - Period: 15, - - // Don't forget to map the provided services from above! - ServicesMap: components.Services{ - setMax_temp.SubPath: &setMax_temp, - setMin_temp.SubPath: &setMin_temp, - setMax_price.SubPath: &setMax_price, - setMin_price.SubPath: &setMin_price, - setSEK_price.SubPath: &setSEK_price, - setDesired_temp.SubPath: &setDesired_temp, - }, - } -} - -//////////////////////////////////////////////////////////////////////////////// - -// newUnitAsset creates a new and proper instance of UnitAsset, using settings and -// values loaded from an existing configuration file. -// This function returns an UA instance that is ready to be published and used, -// aswell as a function that can perform any cleanup when the system is shutting down. -func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { - - sProtocol := components.SProtocols(sys.Husk.ProtoPort) - - // the Cervice that is to be consumed by zigbee, there fore the name with the C - - t := &components.Cervice{ - Name: "setpoint", - Protos: sProtocol, - Url: make([]string, 0), - } - - ua := &UnitAsset{ - // Filling in public fields using the given data - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - SEK_price: uac.SEK_price, - Min_price: uac.Min_price, - Max_price: uac.Max_price, - Min_temp: uac.Min_temp, - Max_temp: uac.Max_temp, - Desired_temp: uac.Desired_temp, - Period: uac.Period, - CervicesMap: components.Cervices{ - t.Name: t, - }, - } - - var ref components.Service - for _, s := range servs { - if s.Definition == "desired_temp" { - ref = s - } - } - - ua.CervicesMap["setpoint"].Details = components.MergeDetails(ua.Details, ref.Details) - - // ua.CervicesMap["setPoint"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}) - - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - go ua.API_feedbackLoop(sys.Ctx) - - // Optionally start background tasks here! Example: - go func() { - log.Println("Starting up " + ua.Name) - }() - - // Returns the loaded unit asset and an function to handle optional cleanup at shutdown - return ua, func() { - log.Println("Cleaning up " + ua.Name) - } -} - -// getSEK_price is used for reading the current hourly electric price -func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.SEK_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setSEK_price updates the current electric price with the new current electric hourly price -func (ua *UnitAsset) setSEK_price(f forms.SignalA_v1a) { - ua.SEK_price = f.Value - log.Printf("new electric price: %.1f", f.Value) -} - -///////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////// - -// getMin_price is used for reading the current value of Min_price -func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Min_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setMin_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { - ua.Min_price = f.Value - log.Printf("new minimum price: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMax_price is used for reading the current value of Max_price -func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Max_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setMax_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { - ua.Max_price = f.Value - log.Printf("new maximum price: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMin_temp is used for reading the current minimum temerature value -func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Min_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -// setMin_temp updates the current minimum temperature set by the user with a new value -func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { - ua.Min_temp = f.Value - log.Printf("new minimum temperature: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMax_temp is used for reading the current value of Min_price -func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Max_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -// setMax_temp updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { - ua.Max_temp = f.Value - log.Printf("new maximum temperature: %.1f", f.Value) - ua.processFeedbackLoop() -} - -func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Desired_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -func (ua *UnitAsset) setDesired_temp(f forms.SignalA_v1a) { - ua.Desired_temp = f.Value - log.Printf("new desired temperature: %.1f", f.Value) -} - -//TODO: This fuction is used for checking the electric price ones every x hours and so on -//TODO: Needs to be modified to match our needs, not using processFeedbacklopp -//TODO: So mayby the period is every hour, call the api to receive the current price ( could be every 24 hours) -//TODO: This function is may be better in the COMFORTSTAT MAIN - -// It's _strongly_ encouraged to not send requests to the API for more than once per hour. -// Making this period a private constant prevents a user from changing this value -// in the config file. -const apiFetchPeriod int = 3600 - -// feedbackLoop is THE control loop (IPR of the system) -func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) - defer ticker.Stop() - - // start the control loop - for { - retrieveAPI_price(ua) - select { - case <-ticker.C: - // Block the loop until the next period - case <-ctx.Done(): - return - } - } -} - -func retrieveAPI_price(ua *UnitAsset) { - url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) - log.Println("URL:", url) - - res, err := http.Get(url) - if err != nil { - log.Println("Couldn't get the url, error:", err) - return - } - body, err := io.ReadAll(res.Body) // Read the payload into body variable - if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) - return - } - var data []API_data // Create a list to hold the gateway json - err = json.Unmarshal(body, &data) // "unpack" body from []byte to []discoverJSON, save errors - res.Body.Close() // defer res.Body.Close() - - if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) - return - } - if err != nil { - log.Println("Error during Unmarshal, error:", err) - return - } - - ///////// - now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) - for _, i := range data { - if i.Time_start == now { - ua.SEK_price = i.SEK_price - log.Println("Price in loop is:", i.SEK_price) - } - - } - log.Println("current el-pris is:", ua.SEK_price) - - // Don't send temperature updates if the difference is too low - // (this could potentially save on battery!) - new_temp := ua.calculateDesiredTemp() - if math.Abs(ua.Desired_temp-new_temp) < 0.5 { - return - } - ua.Desired_temp = new_temp -} - -// feedbackLoop is THE control loop (IPR of the system) -func (ua *UnitAsset) feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(ua.Period * time.Second) - defer ticker.Stop() - - // start the control loop - for { - select { - case <-ticker.C: - ua.processFeedbackLoop() // either modifiy processFeedback loop or write a new one - case <-ctx.Done(): - return - } - } -} - -// - -func (ua *UnitAsset) processFeedbackLoop() { - // get the current temperature - /* - tf, err := usecases.GetState(ua.CervicesMap["setpoint"], ua.Owner) - if err != nil { - log.Printf("\n unable to obtain a setpoint reading error: %s\n", err) - return - } - // Perform a type assertion to convert the returned Form to SignalA_v1a - tup, ok := tf.(*forms.SignalA_v1a) - if !ok { - log.Println("problem unpacking the setpoint signal form") - return - } - */ - /* - miT := ua.getMin_temp().Value - maT := ua.getMax_temp().Value - miP := ua.getMin_price().Value - maP := ua.getMax_price().Value - */ - //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) - // Only send temperature update when we have a new value. - if ua.Desired_temp == ua.old_desired_temp { - return - } - // Keep track of previous value - ua.old_desired_temp = ua.Desired_temp - - // perform the control algorithm - // ua.deviation = ua.Setpt - tup.Value - // output := ua.calculateOutput(ua.deviation) - - // prepare the form to send - var of forms.SignalA_v1a - of.NewForm() - of.Value = ua.Desired_temp - of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] - of.Timestamp = time.Now() - - // pack the new valve state form - op, err := usecases.Pack(&of, "application/json") - if err != nil { - return - } - // send the new valve state request - err = usecases.SetState(ua.CervicesMap["setpoint"], ua.Owner, op) - if err != nil { - log.Printf("cannot update zigbee setpoint: %s\n", err) - return - } -} - -func (ua *UnitAsset) calculateDesiredTemp() float64 { - if ua.SEK_price <= ua.Min_price { - return ua.Max_temp - } - if ua.SEK_price >= ua.Max_price { - return ua.Min_temp - } - - k := -(ua.Max_temp - ua.Min_temp) / (ua.Max_price - ua.Min_price) - //m := max_temp - (k * min_price) - //m := max_temp - desired_temp := k*(ua.SEK_price-ua.Min_price) + ua.Min_temp // y - y_min = k*(x-x_min), solve for y ("desired temp") - return desired_temp -} diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 12d44a5..6de8564 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -48,8 +48,8 @@ func main() { if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) - defer cleanup() + ua, startup := newResource(uac, &sys, servsTemp) + startup() sys.UAssets[ua.GetName()] = &ua } @@ -59,6 +59,9 @@ func main() { // Register the (system) and its services usecases.RegisterServices(&sys) + // Find zigbee gateway and store it in a global variable for reuse + findGateway() + // start the http handler and server go usecases.SetoutServers(&sys) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 1f352fe..e65f087 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -126,8 +126,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi }, } - findGateway(ua) - var ref components.Service for _, s := range servs { if s.Definition == "setpoint" { @@ -137,15 +135,13 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) - if uac.Model == "SmartThermostat" { - ua.sendSetPoint() - } else if uac.Model == "SmartPlug" { - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - } - return ua, func() { - log.Println("Shutting down zigbeevalve ", ua.Name) + if ua.Model == "SmartThermostat" { + ua.sendSetPoint() + } else if ua.Model == "SmartPlug" { + // start the unit asset(s) + go ua.feedbackLoop(ua.Owner.Ctx) + } } } @@ -190,7 +186,9 @@ func (ua *UnitAsset) processFeedbackLoop() { } -func findGateway(ua *UnitAsset) { +var gateway string + +func findGateway() { // https://pkg.go.dev/net/http#Get // GET https://phoscon.de/discover // to find gateways, array of JSONs is returned in http body, we'll only have one so take index 0 // GET the gateway through phoscons built in discover tool, the get will return a response, and in its body an array with JSON elements @@ -222,7 +220,7 @@ func findGateway(ua *UnitAsset) { } // Save the gateway to our unitasset s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) - ua.gateway = s + gateway = s //log.Println("Gateway found:", s) } @@ -247,7 +245,7 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) sendSetPoint() { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + ua.gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload @@ -257,7 +255,7 @@ func (ua *UnitAsset) sendSetPoint() { func (ua *UnitAsset) toggleState(state bool) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + ua.gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 009246d..eecefaf 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -138,10 +138,36 @@ func TestNewResource(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var uac UnitAsset + uac := UnitAsset{ + Name: "Test", + Model: "SmartThermostat", + } + sys := components.NewSystem("testsys", ctx) - servsTemp := []components.Service{} - ua, cleanup := newResource(uac, &sys, servsTemp) + sys.Husk = &components.Husk{ + Description: " is a controller for smart thermostats connected with a RaspBee II", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", + } + + setPointService := components.Service{ + Definition: "setpoint", + SubPath: "setpoint", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current thermal setpoint (GET) or sets it (PUT)", + } + // TODO: fix servsTemp and make sure the test works + servsTemp := components.Service{ + setPointService.SubPath: &setPointService, + } + + ua, _ := newResource(uac, &sys, servsTemp) + + if ua.GetName() != uac.Name { + t.Errorf("Expected ua.Name to be %s, but it was %s", uac.Name, ua.GetName()) + } } From 9ea6be5007e7ba0524f7f39771a7fed4fc8f5772 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 02:53:43 +0100 Subject: [PATCH 010/102] Fixed test for newResource, now working --- ZigBeeValve/zigbee_test.go | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index eecefaf..c594335 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/json" "fmt" "io" + "log" "net/http" "strings" "testing" @@ -11,6 +13,7 @@ import ( "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" ) // mockTransport is used for replacing the default network Transport (used by @@ -138,11 +141,6 @@ func TestNewResource(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - uac := UnitAsset{ - Name: "Test", - Model: "SmartThermostat", - } - sys := components.NewSystem("testsys", ctx) sys.Husk = &components.Husk{ @@ -153,21 +151,22 @@ func TestNewResource(t *testing.T) { InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", } - setPointService := components.Service{ - Definition: "setpoint", - SubPath: "setpoint", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the current thermal setpoint (GET) or sets it (PUT)", - } + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate - // TODO: fix servsTemp and make sure the test works - servsTemp := components.Service{ - setPointService.SubPath: &setPointService, + rawResources, servsTemp, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) } - - ua, _ := newResource(uac, &sys, servsTemp) - - if ua.GetName() != uac.Name { - t.Errorf("Expected ua.Name to be %s, but it was %s", uac.Name, ua.GetName()) + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac UnitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, startup := newResource(uac, &sys, servsTemp) + startup() + sys.UAssets[ua.GetName()] = &ua } } From d6de1892c4bf7b4dd31e9ef851a0534f7ba248d3 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 03:06:23 +0100 Subject: [PATCH 011/102] added systemconfig template because it's needed for the tests --- ZigBeeValve/systemconfig.json | 74 +++++++++++++++++++++++++++++++++++ ZigBeeValve/thing.go | 2 +- ZigBeeValve/zigbee_test.go | 1 + 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 ZigBeeValve/systemconfig.json diff --git a/ZigBeeValve/systemconfig.json b/ZigBeeValve/systemconfig.json new file mode 100644 index 0000000..ae507a5 --- /dev/null +++ b/ZigBeeValve/systemconfig.json @@ -0,0 +1,74 @@ +{ + "systemname": "testsys", + "unit_assets": [ + { + "name": "Template", + "details": { + "Location": [ + "Kitchen" + ] + }, + "model": "", + "period": 10, + "setpoint": 20, + "APIkey": "" + } + ], + "services": [ + { + "servicedefinition": "setpoint", + "details": { + "Forms": [ + "SignalA_v1a" + ], + "Unit": [ + "Celsius" + ] + }, + "registrationPeriod": 0, + "costUnit": "" + } + ], + "protocolsNports": { + "coap": 0, + "http": 8870, + "https": 0 + }, + "distinguishedName": { + "Country": [ + "SE" + ], + "Organization": [ + "Luleaa University of Technology" + ], + "OrganizationalUnit": [ + "CPS" + ], + "Locality": [ + "Luleaa" + ], + "Province": [ + "Norrbotten" + ], + "StreetAddress": null, + "PostalCode": null, + "SerialNumber": "", + "CommonName": "arrowhead.eu", + "Names": null, + "ExtraNames": null + }, + "coreSystems": [ + { + "coresystem": "serviceregistrar", + "url": "http://localhost:8443/serviceregistrar/registry" + }, + { + "coresystem": "orchestrator", + "url": "http://localhost:8445/orchestrator/orchestration" + }, + { + "coresystem": "ca", + "url": "http://localhost:9000/ca/certification" + } + ] +} \ No newline at end of file diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index e65f087..ae7f497 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -254,7 +254,7 @@ func (ua *UnitAsset) sendSetPoint() { } func (ua *UnitAsset) toggleState(state bool) { - // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config + // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" // Create http friendly payload diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index c594335..1f397f5 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -159,6 +159,7 @@ func TestNewResource(t *testing.T) { if err != nil { log.Fatalf("Configuration error: %v\n", err) } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { var uac UnitAsset From 99963489a0b0affd0d4e7ae9266d34aa84f79d84 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 04:01:59 +0100 Subject: [PATCH 012/102] test --- Makefile | 11 +++++------ ZigBeeValve/zigbee_test.go | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 6b6d159..b582289 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +# Updates 3rd party packages and tools +deps: + go mod download + go install github.com/securego/gosec/v2/cmd/gosec@latest + go install github.com/fzipp/gocyclo/cmd/gocyclo@latest # Run tests and log the test coverage test: @@ -21,12 +26,6 @@ analyse: @echo -e "\nCYCLOMATIC COMPLEXITY\n====================" gocyclo -avg -top 10 . -# Updates 3rd party packages and tools -deps: - go mod download - go install github.com/securego/gosec/v2/cmd/gosec@latest - go install github.com/fzipp/gocyclo/cmd/gocyclo@latest - # Show documentation of public parts of package, in the current dir docs: go doc -all diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 1f397f5..afe77a2 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -171,3 +171,43 @@ func TestNewResource(t *testing.T) { sys.UAssets[ua.GetName()] = &ua } } + +func TestfeedbackLoop() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // instantiate the System + sys := components.NewSystem("testsys", ctx) + + // Instatiate the Capusle + sys.Husk = &components.Husk{ + Description: " is a controller for smart thermostats connected with a RaspBee II", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + + // Configure the system + rawResources, servsTemp, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) + } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac UnitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, startup := newResource(uac, &sys, servsTemp) + startup() + sys.UAssets[ua.GetName()] = &ua + } + + // TODO: Test feedbackloop and processfeedbackloop +} From 50d30ec49afef0468f082a5fb34c124379af1f1a Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 04:07:07 +0100 Subject: [PATCH 013/102] test2 --- Makefile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index b582289..3a509b6 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,3 @@ -# Updates 3rd party packages and tools -deps: - go mod download - go install github.com/securego/gosec/v2/cmd/gosec@latest - go install github.com/fzipp/gocyclo/cmd/gocyclo@latest # Run tests and log the test coverage test: @@ -14,6 +9,8 @@ bench: # Runs source code linters and catches common errors lint: + enable: + - gocyclo test -z $$(gofmt -l .) || (echo "Code isn't gofmt'ed!" && exit 1) go vet $$(go list ./... | grep -v /tmp) gosec -quiet -fmt=golint -exclude-dir="tmp" ./... @@ -26,6 +23,12 @@ analyse: @echo -e "\nCYCLOMATIC COMPLEXITY\n====================" gocyclo -avg -top 10 . +# Updates 3rd party packages and tools +deps: + go mod download + go install github.com/securego/gosec/v2/cmd/gosec@latest + go install github.com/fzipp/gocyclo/cmd/gocyclo@latest + # Show documentation of public parts of package, in the current dir docs: go doc -all From 4a1a5d7ebdfc14609f0da7ef3d443530c3af3f71 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 04:08:08 +0100 Subject: [PATCH 014/102] Makefile back to normal --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 3a509b6..6b6d159 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,6 @@ bench: # Runs source code linters and catches common errors lint: - enable: - - gocyclo test -z $$(gofmt -l .) || (echo "Code isn't gofmt'ed!" && exit 1) go vet $$(go list ./... | grep -v /tmp) gosec -quiet -fmt=golint -exclude-dir="tmp" ./... From ba91d61d6725f4e0a7b09f85cb635772efcdb287 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 12:06:01 +0100 Subject: [PATCH 015/102] Fixed workflow (git tests) --- .github/workflows/main.yml | 2 ++ ZigBeeValve/thing.go | 2 -- ZigBeeValve/zigbee_test.go | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d0f615..d95f148 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.23 + - name: Install dependencies + run: make deps - name: Run tests run: make test - name: Report stats diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index ae7f497..a91bf49 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -182,8 +182,6 @@ func (ua *UnitAsset) processFeedbackLoop() { } else { ua.toggleState(false) } - //log.Println("Feedback loop done.") - } var gateway string diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index afe77a2..db5c28f 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -166,20 +166,18 @@ func TestNewResource(t *testing.T) { if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, startup := newResource(uac, &sys, servsTemp) - startup() + ua, _ := newResource(uac, &sys, servsTemp) + //startup() sys.UAssets[ua.GetName()] = &ua } } -func TestfeedbackLoop() { +func TestFeedbackLoop(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - // instantiate the System sys := components.NewSystem("testsys", ctx) - // Instatiate the Capusle sys.Husk = &components.Husk{ Description: " is a controller for smart thermostats connected with a RaspBee II", Certificate: "ABCD", @@ -188,26 +186,30 @@ func TestfeedbackLoop() { InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", } - // instantiate a template unit asset assetTemplate := initTemplate() assetName := assetTemplate.GetName() sys.UAssets[assetName] = &assetTemplate - // Configure the system rawResources, servsTemp, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { var uac UnitAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, startup := newResource(uac, &sys, servsTemp) - startup() + ua, _ := newResource(uac, &sys, servsTemp) + //startup() sys.UAssets[ua.GetName()] = &ua } // TODO: Test feedbackloop and processfeedbackloop + /* + ua.feedbackLoop(ctx) + cancel() + time.Sleep(2 * time.Second) + */ } From 665be03818740573c64893d6329516a3331e84fe Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 28 Jan 2025 14:05:25 +0100 Subject: [PATCH 016/102] Added new tests, work in progress --- ZigBeeValve/thing.go | 22 +++---- ZigBeeValve/zigbee_test.go | 121 +++++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 66 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index a91bf49..26c155c 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -18,8 +18,7 @@ import ( "github.com/sdoque/mbaigo/usecases" ) -//------------------------------------ Used when discovering the gateway - +// ------------------------------------ Used when discovering the gateway type discoverJSON struct { Id string `json:"id"` Internalipaddress string `json:"internalipaddress"` @@ -39,11 +38,10 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Model string `json:"model"` - Period time.Duration `json:"period"` - Setpt float64 `json:"setpoint"` - gateway string - Apikey string `json:"APIkey"` + Model string `json:"model"` + Period time.Duration `json:"period"` + Setpt float64 `json:"setpoint"` + Apikey string `json:"APIkey"` } // GetName returns the name of the Resource. @@ -87,8 +85,7 @@ func initTemplate() components.UnitAsset { Model: "", Period: 10, Setpt: 20, - gateway: "", - Apikey: "", + Apikey: "1234", ServicesMap: components.Services{ setPointService.SubPath: &setPointService, }, @@ -119,7 +116,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi Model: uac.Model, Period: uac.Period, Setpt: uac.Setpt, - gateway: uac.gateway, Apikey: uac.Apikey, CervicesMap: components.Cervices{ t.Name: t, @@ -216,10 +212,10 @@ func findGateway() { log.Println("No gateway was found") return } - // Save the gateway to our unitasset + // Save the gateway s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) gateway = s - //log.Println("Gateway found:", s) + log.Println("Gateway found:", s) } //-------------------------------------Thing's resource methods @@ -281,7 +277,7 @@ func sendRequest(data []byte, apiURL string) { defer resp.Body.Close() b, err := io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) + log.Println("Something went wrong while reading payload into body variable, error:", err) return } if resp.StatusCode > 299 { diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index db5c28f..8181516 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -9,7 +9,6 @@ import ( "net/http" "strings" "testing" - "time" "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" @@ -45,21 +44,21 @@ func (t mockTransport) domainHits(domain string) int { // TODO: this might need to be expanded to a full JSON array? -const priceExample string = `[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" -}]` +const discoverExample string = `[{ + "Id": "123", + "Internalipaddress": "localhost", + "Macaddress": "test", + "Internalport": 8080, + "Name": "My gateway", + "Publicipaddress": "test" + }]` // RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). // It prevents the request from being sent over the network and count how many times // a domain was requested. func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) + fakeBody := fmt.Sprint(discoverExample) // TODO: should be able to adjust these return values for the error cases resp = &http.Response{ Status: "200 OK", @@ -73,14 +72,7 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er //////////////////////////////////////////////////////////////////////////////// -const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" -const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" - func TestUnitAsset(t *testing.T) { - - // Don't understand how to check my own deConz API calls, will extend the test with this once i understand - trans := newMockTransport() - // Create a form f := forms.SignalA_v1a{ Value: 27.0, @@ -96,10 +88,11 @@ func TestUnitAsset(t *testing.T) { t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) } - // TODO: Add api call to make sure it only sends update to HW once. - hits := trans.domainHits(thermostatDomain) - if hits > 1 { - t.Errorf("Expected number of api requests = 1, got %d requests", hits) + // Fetch Setpt w/ a form + f2 := ua.getSetPoint() + + if f2.Value != f.Value { + t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) } } @@ -172,44 +165,66 @@ func TestNewResource(t *testing.T) { } } -func TestFeedbackLoop(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sys := components.NewSystem("testsys", ctx) +const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" +const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" - sys.Husk = &components.Husk{ - Description: " is a controller for smart thermostats connected with a RaspBee II", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, - InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", - } +func TestProcessfeedbackLoop(t *testing.T) { + // Don't know how to test this +} - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate +func TestFindGateway1(t *testing.T) { + // New mocktransport + gatewayDomain := "https://phoscon.de/discover" + trans := newMockTransport() - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) + // ---- All ok! ---- + findGateway() + if gateway != "localhost:8080" { + t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } - - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, _ := newResource(uac, &sys, servsTemp) - //startup() - sys.UAssets[ua.GetName()] = &ua + hits := trans.domainHits(gatewayDomain) + if hits > 1 { + t.Fatalf("Too many hits on gatewayDomain, expected 1 got, %d", hits) } - // TODO: Test feedbackloop and processfeedbackloop + // Have to make changes to mockTransport to test this? + // ---- Error cases ---- /* - ua.feedbackLoop(ctx) - cancel() - time.Sleep(2 * time.Second) + // Couldn't find gateway + findGateway() + if gateway != "" { + log.Printf("Expected not to find gateway, found %s", gateway) + } */ + // Statuscode > 299, have to make changes to mockTransport to test this + // Couldn't read body, have to make changes to mockTransport to test this + // Error during unmarshal, have to make changes to mockTransport to test this +} + +const zigbeeGateway string = "http://localhost:8080/" + +func TestToggleState(t *testing.T) { + trans := newMockTransport() + + ua := initTemplate().(*UnitAsset) + + ua.toggleState(true) + + hits := trans.domainHits(plugDomain) + if hits > 1 { + t.Errorf("Expected one hit, got %d", hits) + } +} + +func TestSendSetPoint(t *testing.T) { + trans := newMockTransport() + + ua := initTemplate().(*UnitAsset) + + ua.sendSetPoint() + + hits := trans.domainHits(thermostatDomain) + if hits > 1 { + t.Errorf("expected one hit, got %d", hits) + } } From 39a4dc458188b1c4469e8a547049c5a3cb105d12 Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 28 Jan 2025 17:41:38 +0100 Subject: [PATCH 017/102] Added new tests etc --- ZigBeeValve/ZigBeeValve.go | 11 ++- ZigBeeValve/thing.go | 73 ++++++++++++-------- ZigBeeValve/zigbee_test.go | 135 +++++++++++++++++++++++++------------ 3 files changed, 148 insertions(+), 71 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 6de8564..9fce96a 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -60,7 +60,10 @@ func main() { usecases.RegisterServices(&sys) // Find zigbee gateway and store it in a global variable for reuse - findGateway() + err = findGateway() + if err != nil { + log.Fatal("Error getting gateway, shutting down:", err) + } // start the http handler and server go usecases.SetoutServers(&sys) @@ -97,7 +100,11 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { log.Println("Model:", rsc.Model) rsc.setSetPoint(sig) if rsc.Model == "SmartThermostat" { - rsc.sendSetPoint() + err = rsc.sendSetPoint() + if err != nil { + log.Println("Error sending setpoint:", err) + http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) + } } default: diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 26c155c..cfd3038 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -133,7 +133,11 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi return ua, func() { if ua.Model == "SmartThermostat" { - ua.sendSetPoint() + err := ua.sendSetPoint() + if err != nil { + log.Println("Error occured:", err) + // TODO: Turn off system if this startup() fails + } } else if ua.Model == "SmartPlug" { // start the unit asset(s) go ua.feedbackLoop(ua.Owner.Ctx) @@ -174,48 +178,61 @@ func (ua *UnitAsset) processFeedbackLoop() { // TODO: Check diff instead of a hard over/under value? meaning it'll only turn on/off if diff is over 0.5 degrees if tup.Value < ua.Setpt { - ua.toggleState(true) + err = ua.toggleState(true) + if err != nil { + log.Println("Error occured: ", err) + } } else { - ua.toggleState(false) + err = ua.toggleState(false) + if err != nil { + log.Println("Error occured: ", err) + } } } var gateway string -func findGateway() { +const discoveryURL string = "https://phoscon.de/discover" + +var errStatusCode error = fmt.Errorf("bad status code") +var errMissingGateway error = fmt.Errorf("missing gateway") + +func findGateway() (err error) { // https://pkg.go.dev/net/http#Get // GET https://phoscon.de/discover // to find gateways, array of JSONs is returned in http body, we'll only have one so take index 0 // GET the gateway through phoscons built in discover tool, the get will return a response, and in its body an array with JSON elements // ours is index 0 since there's no other RaspBee/ZigBee gateways on the network - res, err := http.Get("https://phoscon.de/discover") + res, err := http.Get(discoveryURL) if err != nil { - log.Println("Couldn't get gateway, error:", err) + //log.Println("Couldn't get gateway, error:", err) return } defer res.Body.Close() if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\n", res.StatusCode) - return + //log.Printf("Response failed with status code: %d and\n", res.StatusCode) + return errStatusCode } body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) + //log.Println("Something went wrong while reading the body during discovery, error:", err) return } var gw []discoverJSON // Create a list to hold the gateway json err = json.Unmarshal(body, &gw) // "unpack" body from []byte to []discoverJSON, save errors if err != nil { - log.Println("Error during Unmarshal, error:", err) + //log.Println("Error during Unmarshal, error:", err) return } + if len(gw) < 1 { - log.Println("No gateway was found") - return + //log.Println("No gateway was found") + return errMissingGateway } // Save the gateway s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) gateway = s - log.Println("Gateway found:", s) + //log.Println("Gateway found:", s) + return } //-------------------------------------Thing's resource methods @@ -237,33 +254,34 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { log.Println("*---------------------*") } -func (ua *UnitAsset) sendSetPoint() { +func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload data := []byte(s) // Turned into byte array - sendRequest(data, apiURL) + return sendRequest(data, apiURL) } -func (ua *UnitAsset) toggleState(state bool) { +func (ua *UnitAsset) toggleState(state bool) (err error) { // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload data := []byte(s) // Turned into byte array - sendRequest(data, apiURL) + err = sendRequest(data, apiURL) + return err } -func sendRequest(data []byte, apiURL string) { +func sendRequest(data []byte, apiURL string) (err error) { body := bytes.NewBuffer(data) // Put data into buffer req, err := http.NewRequest(http.MethodPut, apiURL, body) // Put request is made if err != nil { - log.Println("Error making new HTTP PUT request, error:", err) - return + // log.Println("Error making new HTTP PUT request, error:", err) + return err } req.Header.Set("Content-Type", "application/json") // Make sure it's JSON @@ -271,17 +289,18 @@ func sendRequest(data []byte, apiURL string) { client := &http.Client{} // Make a client resp, err := client.Do(req) // Perform the put request if err != nil { - log.Println("Error sending HTTP PUT request, error:", err) - return + // log.Println("Error sending HTTP PUT request, error:", err) + return err } defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) // Read the payload into body variable + _, err = io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { - log.Println("Something went wrong while reading payload into body variable, error:", err) - return + // log.Println("Something went wrong while reading payload into body variable, error:", err) + return err } if resp.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) - return + // log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) + return err } + return } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 8181516..262a721 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -19,12 +19,16 @@ import ( // http.DefaultClient) and it will intercept network requests. type mockTransport struct { + resp *http.Response hits map[string]int + err error } -func newMockTransport() mockTransport { +func newMockTransport(resp *http.Response, err error) mockTransport { t := mockTransport{ + resp: resp, hits: make(map[string]int), + err: err, } // Highjack the default http client so no actuall http requests are sent over the network http.DefaultClient.Transport = t @@ -58,16 +62,12 @@ const discoverExample string = `[{ // a domain was requested. func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - fakeBody := fmt.Sprint(discoverExample) - // TODO: should be able to adjust these return values for the error cases - resp = &http.Response{ - Status: "200 OK", - StatusCode: 200, - Request: req, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } + t.resp.Request = req t.hits[req.URL.Hostname()] += 1 - return + if t.err != nil { + return nil, t.err + } + return t.resp, nil } //////////////////////////////////////////////////////////////////////////////// @@ -168,63 +168,114 @@ func TestNewResource(t *testing.T) { const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" -func TestProcessfeedbackLoop(t *testing.T) { - // Don't know how to test this +type errReader int + +var errBodyRead error = fmt.Errorf("bad body read") + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} + +func (errReader) Close() error { + return nil } -func TestFindGateway1(t *testing.T) { +func TestFindGateway(t *testing.T) { // New mocktransport - gatewayDomain := "https://phoscon.de/discover" - trans := newMockTransport() + //gatewayDomain := "https://phoscon.de/discover" + + fakeBody := fmt.Sprint(discoverExample) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + newMockTransport(resp, nil) // ---- All ok! ---- - findGateway() + err := findGateway() + + if err != nil { + t.Fatal("Gatewayn not found", err) + } if gateway != "localhost:8080" { t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } - hits := trans.domainHits(gatewayDomain) - if hits > 1 { - t.Fatalf("Too many hits on gatewayDomain, expected 1 got, %d", hits) - } // Have to make changes to mockTransport to test this? // ---- Error cases ---- - /* - // Couldn't find gateway - findGateway() - if gateway != "" { - log.Printf("Expected not to find gateway, found %s", gateway) - } - */ + + // Unmarshall error + newMockTransport(resp, fmt.Errorf("Test error")) + err = findGateway() + if err == nil { + t.Error("Error expcted, got nil instead", err) + } + // Statuscode > 299, have to make changes to mockTransport to test this - // Couldn't read body, have to make changes to mockTransport to test this - // Error during unmarshal, have to make changes to mockTransport to test this -} + resp.StatusCode = 300 + newMockTransport(resp, nil) + err = findGateway() + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } + + // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body + resp.StatusCode = 200 + resp.Body = errReader(0) + newMockTransport(resp, nil) + err = findGateway() + if err != errBodyRead { + t.Error("Expected error") + } + + // Actual http body is unmarshaled correctly + resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) + newMockTransport(resp, nil) + err = findGateway() + if err == nil { + t.Error("Expected error") + } -const zigbeeGateway string = "http://localhost:8080/" + // Empty list of gateways + resp.Body = io.NopCloser(strings.NewReader("[]")) + newMockTransport(resp, nil) + err = findGateway() + if err != errMissingGateway { + t.Error("Expected error", err) + } +} func TestToggleState(t *testing.T) { - trans := newMockTransport() + fakeBody := fmt.Sprint("") + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, nil) ua := initTemplate().(*UnitAsset) ua.toggleState(true) - - hits := trans.domainHits(plugDomain) - if hits > 1 { - t.Errorf("Expected one hit, got %d", hits) - } } func TestSendSetPoint(t *testing.T) { - trans := newMockTransport() + fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, nil) ua := initTemplate().(*UnitAsset) ua.sendSetPoint() - hits := trans.domainHits(thermostatDomain) - if hits > 1 { - t.Errorf("expected one hit, got %d", hits) - } } From 99dc342b9752b1afc1d95677e7c9c8dceb1bbe44 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Wed, 29 Jan 2025 16:53:29 +0100 Subject: [PATCH 018/102] Tested pretty much all the code possible in things.go, moving on to ZigBeeValve.go --- ZigBeeValve/thing.go | 42 +++++----- ZigBeeValve/zigbee_test.go | 158 ++++++++++++++++++++++++++++--------- 2 files changed, 141 insertions(+), 59 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index cfd3038..7669316 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -204,7 +204,6 @@ func findGateway() (err error) { // ours is index 0 since there's no other RaspBee/ZigBee gateways on the network res, err := http.Get(discoveryURL) if err != nil { - //log.Println("Couldn't get gateway, error:", err) return } defer res.Body.Close() @@ -214,13 +213,11 @@ func findGateway() (err error) { } body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { - //log.Println("Something went wrong while reading the body during discovery, error:", err) return } var gw []discoverJSON // Create a list to hold the gateway json err = json.Unmarshal(body, &gw) // "unpack" body from []byte to []discoverJSON, save errors if err != nil { - //log.Println("Error during Unmarshal, error:", err) return } @@ -260,8 +257,11 @@ func (ua *UnitAsset) sendSetPoint() (err error) { // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload - data := []byte(s) // Turned into byte array - return sendRequest(data, apiURL) + req, err := createRequest(s, apiURL) + if err != nil { + return + } + return sendRequest(req) } func (ua *UnitAsset) toggleState(state bool) (err error) { @@ -270,37 +270,37 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload - data := []byte(s) // Turned into byte array - err = sendRequest(data, apiURL) - return err + req, err := createRequest(s, apiURL) + if err != nil { + return + } + return sendRequest(req) } -func sendRequest(data []byte, apiURL string) (err error) { - body := bytes.NewBuffer(data) // Put data into buffer - - req, err := http.NewRequest(http.MethodPut, apiURL, body) // Put request is made +func createRequest(data string, apiURL string) (req *http.Request, err error) { + body := bytes.NewReader([]byte(data)) // Put data into buffer + req, err = http.NewRequest(http.MethodPut, apiURL, body) // Put request is made if err != nil { - // log.Println("Error making new HTTP PUT request, error:", err) - return err + return nil, err } req.Header.Set("Content-Type", "application/json") // Make sure it's JSON + return req, err +} - client := &http.Client{} // Make a client - resp, err := client.Do(req) // Perform the put request +func sendRequest(req *http.Request) (err error) { + resp, err := http.DefaultClient.Do(req) if err != nil { - // log.Println("Error sending HTTP PUT request, error:", err) return err } + defer resp.Body.Close() _, err = io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { - // log.Println("Something went wrong while reading payload into body variable, error:", err) - return err + return } if resp.StatusCode > 299 { - // log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) - return err + return errStatusCode } return } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 262a721..e9d63cb 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -19,33 +19,24 @@ import ( // http.DefaultClient) and it will intercept network requests. type mockTransport struct { - resp *http.Response - hits map[string]int - err error + returnError bool + resp *http.Response + hits map[string]int + err error } -func newMockTransport(resp *http.Response, err error) mockTransport { +func newMockTransport(resp *http.Response, retErr bool, err error) mockTransport { t := mockTransport{ - resp: resp, - hits: make(map[string]int), - err: err, + returnError: retErr, + resp: resp, + hits: make(map[string]int), + err: err, } // Highjack the default http client so no actuall http requests are sent over the network http.DefaultClient.Transport = t return t } -// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). - -func (t mockTransport) domainHits(domain string) int { - for u, hits := range t.hits { - if u == domain { - return hits - } - } - return -1 -} - // TODO: this might need to be expanded to a full JSON array? const discoverExample string = `[{ @@ -61,12 +52,19 @@ const discoverExample string = `[{ // It prevents the request from being sent over the network and count how many times // a domain was requested. +var errHTTP error = fmt.Errorf("bad http request") + func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - t.resp.Request = req t.hits[req.URL.Hostname()] += 1 if t.err != nil { return nil, t.err } + if t.returnError != false { + req.GetBody = func() (io.ReadCloser, error) { + return nil, errHTTP + } + } + t.resp.Request = req return t.resp, nil } @@ -165,9 +163,6 @@ func TestNewResource(t *testing.T) { } } -const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" -const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" - type errReader int var errBodyRead error = fmt.Errorf("bad body read") @@ -180,10 +175,25 @@ func (errReader) Close() error { return nil } -func TestFindGateway(t *testing.T) { - // New mocktransport - //gatewayDomain := "https://phoscon.de/discover" +func TestProcessFeedbackLoop(t *testing.T) { + // TODO: Test as much of the code as possible. + // Maybe try to pass arguments to processFeedbackLoop, to skip the GetState() function? + /* + fakeBody := // Find out what a form looks like, and pass it to this test function + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + newMockTransport(resp, false, nil) + + ua := initTemplate().(*UnitAsset) + ua.processFeedbackLoop() + */ +} +func TestFindGateway(t *testing.T) { fakeBody := fmt.Sprint(discoverExample) resp := &http.Response{ @@ -191,7 +201,7 @@ func TestFindGateway(t *testing.T) { StatusCode: 200, Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) // ---- All ok! ---- err := findGateway() @@ -203,11 +213,10 @@ func TestFindGateway(t *testing.T) { t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } - // Have to make changes to mockTransport to test this? // ---- Error cases ---- // Unmarshall error - newMockTransport(resp, fmt.Errorf("Test error")) + newMockTransport(resp, false, fmt.Errorf("Test error")) err = findGateway() if err == nil { t.Error("Error expcted, got nil instead", err) @@ -215,7 +224,7 @@ func TestFindGateway(t *testing.T) { // Statuscode > 299, have to make changes to mockTransport to test this resp.StatusCode = 300 - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err != errStatusCode { t.Error("Expected errStatusCode, got", err) @@ -224,7 +233,7 @@ func TestFindGateway(t *testing.T) { // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body resp.StatusCode = 200 resp.Body = errReader(0) - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err != errBodyRead { t.Error("Expected error") @@ -232,7 +241,7 @@ func TestFindGateway(t *testing.T) { // Actual http body is unmarshaled correctly resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err == nil { t.Error("Expected error") @@ -240,7 +249,7 @@ func TestFindGateway(t *testing.T) { // Empty list of gateways resp.Body = io.NopCloser(strings.NewReader("[]")) - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err != errMissingGateway { t.Error("Expected error", err) @@ -248,7 +257,7 @@ func TestFindGateway(t *testing.T) { } func TestToggleState(t *testing.T) { - fakeBody := fmt.Sprint("") + fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) resp := &http.Response{ Status: "200 OK", @@ -256,11 +265,15 @@ func TestToggleState(t *testing.T) { Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, nil) - + newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) - + // All ok! ua.toggleState(true) + // Error + // change gateway to bad character/url, return gateway to original value + gateway = brokenURL + ua.toggleState(true) + findGateway() } func TestSendSetPoint(t *testing.T) { @@ -272,10 +285,79 @@ func TestSendSetPoint(t *testing.T) { Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, nil) - + newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) + // All ok! + gateway = "" + err := ua.sendSetPoint() + if err != nil { + t.Error("Unexpected error:", err) + } + // Error + gateway = brokenURL ua.sendSetPoint() + findGateway() + +} + +// func createRequest(data string, apiURL string) (req *http.Request, err error) +func TestCreateRequest(t *testing.T) { + data := "test" + apiURL := "http://localhost:8080/test" + + _, err := createRequest(data, apiURL) + if err != nil { + t.Error("Error occured, expected none") + } + + _, err = createRequest(data, brokenURL) + if err == nil { + t.Error("Expected error") + } + +} + +var brokenURL string = string([]byte{0x7f}) + +func TestSendRequest(t *testing.T) { + // Set up standard response & catch http requests + fakeBody := fmt.Sprint(`Test`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + // All ok! + newMockTransport(resp, false, nil) + apiURL := "http://localhost:8080/test" + s := fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload + req, _ := createRequest(s, apiURL) + err := sendRequest(req) + if err != nil { + t.Error("Expected no errors, error occured:", err) + } + + // Error unpacking body + resp.Body = errReader(0) + newMockTransport(resp, false, nil) + + err = sendRequest(req) + + if err == nil { + t.Error("Expected errors, no error occured:") + } + + // Error StatusCode + resp.Body = io.NopCloser(strings.NewReader(fakeBody)) + resp.StatusCode = 300 + newMockTransport(resp, false, nil) + err = sendRequest(req) + + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } } From e54fbd535251bf45d34837a29e6a2bbdf95acb78 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 3 Feb 2025 11:40:41 +0100 Subject: [PATCH 019/102] changed tests, and added new tests --- ZigBeeValve/ZigBeeValve.go | 13 +- ZigBeeValve/systemconfig.json | 74 ------ ZigBeeValve/thing.go | 10 +- ZigBeeValve/thing_test.go | 353 +++++++++++++++++++++++++++++ ZigBeeValve/zigbee_test.go | 408 ++++++---------------------------- 5 files changed, 429 insertions(+), 429 deletions(-) delete mode 100644 ZigBeeValve/systemconfig.json create mode 100644 ZigBeeValve/thing_test.go diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 9fce96a..148741f 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -62,7 +62,7 @@ func main() { // Find zigbee gateway and store it in a global variable for reuse err = findGateway() if err != nil { - log.Fatal("Error getting gateway, shutting down:", err) + log.Fatal("Error getting gateway, shutting down: ", err) } // start the http handler and server @@ -93,17 +93,18 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting desired temp ", err) + log.Println("Error processing request") + http.Error(w, "Request incorrectly formated", http.StatusBadRequest) + return } - log.Println("sig:", sig) - log.Println("URL:", r.URL) - log.Println("Model:", rsc.Model) + rsc.setSetPoint(sig) if rsc.Model == "SmartThermostat" { err = rsc.sendSetPoint() if err != nil { - log.Println("Error sending setpoint:", err) + log.Println("Error sending setpoint:") http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) + return } } diff --git a/ZigBeeValve/systemconfig.json b/ZigBeeValve/systemconfig.json deleted file mode 100644 index ae507a5..0000000 --- a/ZigBeeValve/systemconfig.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "systemname": "testsys", - "unit_assets": [ - { - "name": "Template", - "details": { - "Location": [ - "Kitchen" - ] - }, - "model": "", - "period": 10, - "setpoint": 20, - "APIkey": "" - } - ], - "services": [ - { - "servicedefinition": "setpoint", - "details": { - "Forms": [ - "SignalA_v1a" - ], - "Unit": [ - "Celsius" - ] - }, - "registrationPeriod": 0, - "costUnit": "" - } - ], - "protocolsNports": { - "coap": 0, - "http": 8870, - "https": 0 - }, - "distinguishedName": { - "Country": [ - "SE" - ], - "Organization": [ - "Luleaa University of Technology" - ], - "OrganizationalUnit": [ - "CPS" - ], - "Locality": [ - "Luleaa" - ], - "Province": [ - "Norrbotten" - ], - "StreetAddress": null, - "PostalCode": null, - "SerialNumber": "", - "CommonName": "arrowhead.eu", - "Names": null, - "ExtraNames": null - }, - "coreSystems": [ - { - "coresystem": "serviceregistrar", - "url": "http://localhost:8443/serviceregistrar/registry" - }, - { - "coresystem": "orchestrator", - "url": "http://localhost:8445/orchestrator/orchestration" - }, - { - "coresystem": "ca", - "url": "http://localhost:9000/ca/certification" - } - ] -} \ No newline at end of file diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 7669316..2fc9689 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -82,7 +82,7 @@ func initTemplate() components.UnitAsset { uat := &UnitAsset{ Name: "Template", Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "", + Model: "SmartThermostat", Period: 10, Setpt: 20, Apikey: "1234", @@ -246,9 +246,11 @@ func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { // setSetPoint updates the thermal setpoint func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { ua.Setpt = f.Value - log.Println("*---------------------*") - log.Printf("New set point: %.1f\n", f.Value) - log.Println("*---------------------*") + /* + log.Println("*---------------------*") + log.Printf("New set point: %.1f\n", f.Value) + log.Println("*---------------------*") + */ } func (ua *UnitAsset) sendSetPoint() (err error) { diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go new file mode 100644 index 0000000..0c6f684 --- /dev/null +++ b/ZigBeeValve/thing_test.go @@ -0,0 +1,353 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" +) + +// mockTransport is used for replacing the default network Transport (used by +// http.DefaultClient) and it will intercept network requests. + +type mockTransport struct { + returnError bool + resp *http.Response + hits map[string]int + err error +} + +func newMockTransport(resp *http.Response, retErr bool, err error) mockTransport { + t := mockTransport{ + returnError: retErr, + resp: resp, + hits: make(map[string]int), + err: err, + } + // Highjack the default http client so no actuall http requests are sent over the network + http.DefaultClient.Transport = t + return t +} + +// TODO: this might need to be expanded to a full JSON array? + +const discoverExample string = `[{ + "Id": "123", + "Internalipaddress": "localhost", + "Macaddress": "test", + "Internalport": 8080, + "Name": "My gateway", + "Publicipaddress": "test" + }]` + +// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). +// It prevents the request from being sent over the network and count how many times +// a domain was requested. + +var errHTTP error = fmt.Errorf("bad http request") + +func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + t.hits[req.URL.Hostname()] += 1 + if t.err != nil { + return nil, t.err + } + if t.returnError != false { + req.GetBody = func() (io.ReadCloser, error) { + return nil, errHTTP + } + } + t.resp.Request = req + return t.resp, nil +} + +//////////////////////////////////////////////////////////////////////////////// + +func TestUnitAsset(t *testing.T) { + // Create a form + f := forms.SignalA_v1a{ + Value: 27.0, + } + + // Creates a single UnitAsset and assert it changes + ua := initTemplate().(*UnitAsset) + + // Change Setpt + ua.setSetPoint(f) + + if ua.Setpt != 27.0 { + t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) + } + + // Fetch Setpt w/ a form + f2 := ua.getSetPoint() + + if f2.Value != f.Value { + t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) + } +} + +func TestGetters(t *testing.T) { + ua := initTemplate().(*UnitAsset) + + name := ua.GetName() + if name != "Template" { + t.Errorf("Expected name to be 2, instead got %s", name) + } + + services := ua.GetServices() + if services == nil { + t.Fatalf("Expected services not to be nil") + } + if services["setpoint"].Definition != "setpoint" { + t.Errorf("Expected definition to be setpoint") + } + + details := ua.GetDetails() + if details == nil { + t.Fatalf("Details was nil, expected map") + } + if len(details["Location"]) == 0 { + t.Fatalf("Location was nil, expected kitchen") + } + + if details["Location"][0] != "Kitchen" { + t.Errorf("Expected location to be Kitchen") + } + + cervices := ua.GetCervices() + if cervices != nil { + t.Errorf("Expected no cervices") + } +} + +func TestNewResource(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + sys := components.NewSystem("testsys", ctx) + sys.Husk = &components.Husk{ + Description: " is a controller for smart thermostats connected with a RaspBee II", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", + } + + setPointService := components.Service{ + Definition: "setpoint", + SubPath: "setpoint", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current thermal setpoint (GET) or sets it (PUT)", + } + + uac := UnitAsset{ + Name: "Template", + Details: map[string][]string{"Location": {"Kitchen"}}, + Model: "SmartThermostat", + Period: 10, + Setpt: 20, + Apikey: "1234", + ServicesMap: components.Services{ + setPointService.SubPath: &setPointService, + }, + } + + ua, _ := newResource(uac, &sys, nil) + // Happy test case: + //Get a unitasset with correct values + name := ua.GetName() + if name != "Template" { + t.Errorf("Expected name to be Template, but instead got: %v", name) + } +} + +type errReader int + +var errBodyRead error = fmt.Errorf("bad body read") + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} + +func (errReader) Close() error { + return nil +} + +func TestProcessFeedbackLoop(t *testing.T) { + // TODO: Test as much of the code as possible? +} + +func TestFindGateway(t *testing.T) { + fakeBody := fmt.Sprint(discoverExample) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + newMockTransport(resp, false, nil) + + // ---- All ok! ---- + err := findGateway() + + if err != nil { + t.Fatal("Gatewayn not found", err) + } + if gateway != "localhost:8080" { + t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) + } + + // ---- Error cases ---- + + // Unmarshall error + newMockTransport(resp, false, fmt.Errorf("Test error")) + err = findGateway() + if err == nil { + t.Error("Error expcted, got nil instead", err) + } + + // Statuscode > 299, have to make changes to mockTransport to test this + resp.StatusCode = 300 + newMockTransport(resp, false, nil) + err = findGateway() + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } + + // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body + resp.StatusCode = 200 + resp.Body = errReader(0) + newMockTransport(resp, false, nil) + err = findGateway() + if err != errBodyRead { + t.Error("Expected error") + } + + // Actual http body is unmarshaled correctly + resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) + newMockTransport(resp, false, nil) + err = findGateway() + if err == nil { + t.Error("Expected error") + } + + // Empty list of gateways + resp.Body = io.NopCloser(strings.NewReader("[]")) + newMockTransport(resp, false, nil) + err = findGateway() + if err != errMissingGateway { + t.Error("Expected error", err) + } +} + +func TestToggleState(t *testing.T) { + fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, false, nil) + ua := initTemplate().(*UnitAsset) + // All ok! + ua.toggleState(true) + // Error + // change gateway to bad character/url, return gateway to original value + gateway = brokenURL + ua.toggleState(true) + findGateway() +} + +func TestSendSetPoint(t *testing.T) { + fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, false, nil) + ua := initTemplate().(*UnitAsset) + // All ok! + gateway = "localhost" + err := ua.sendSetPoint() + if err != nil { + t.Error("Unexpected error:", err) + } + + // Error + gateway = brokenURL + ua.sendSetPoint() + findGateway() + gateway = "localhost" +} + +// func createRequest(data string, apiURL string) (req *http.Request, err error) +func TestCreateRequest(t *testing.T) { + data := "test" + apiURL := "http://localhost:8080/test" + + _, err := createRequest(data, apiURL) + if err != nil { + t.Error("Error occured, expected none") + } + + _, err = createRequest(data, brokenURL) + if err == nil { + t.Error("Expected error") + } + +} + +var brokenURL string = string([]byte{0x7f}) + +func TestSendRequest(t *testing.T) { + // Set up standard response & catch http requests + fakeBody := fmt.Sprint(`Test`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + // All ok! + newMockTransport(resp, false, nil) + apiURL := "http://localhost:8080/test" + s := fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload + req, _ := createRequest(s, apiURL) + err := sendRequest(req) + if err != nil { + t.Error("Expected no errors, error occured:", err) + } + + // Break defaultClient.Do() + + // Error unpacking body + resp.Body = errReader(0) + newMockTransport(resp, false, nil) + + err = sendRequest(req) + + if err == nil { + t.Error("Expected errors, no error occured:") + } + + // Error StatusCode + resp.Body = io.NopCloser(strings.NewReader(fakeBody)) + resp.StatusCode = 300 + newMockTransport(resp, false, nil) + err = sendRequest(req) + + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } + +} diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index e9d63cb..5cb0b59 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -1,363 +1,81 @@ package main import ( - "context" - "encoding/json" - "fmt" "io" "log" "net/http" + "net/http/httptest" "strings" "testing" + "time" - "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" ) -// mockTransport is used for replacing the default network Transport (used by -// http.DefaultClient) and it will intercept network requests. - -type mockTransport struct { - returnError bool - resp *http.Response - hits map[string]int - err error -} - -func newMockTransport(resp *http.Response, retErr bool, err error) mockTransport { - t := mockTransport{ - returnError: retErr, - resp: resp, - hits: make(map[string]int), - err: err, - } - // Highjack the default http client so no actuall http requests are sent over the network - http.DefaultClient.Transport = t - return t -} - -// TODO: this might need to be expanded to a full JSON array? - -const discoverExample string = `[{ - "Id": "123", - "Internalipaddress": "localhost", - "Macaddress": "test", - "Internalport": 8080, - "Name": "My gateway", - "Publicipaddress": "test" - }]` - -// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). -// It prevents the request from being sent over the network and count how many times -// a domain was requested. - -var errHTTP error = fmt.Errorf("bad http request") - -func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - t.hits[req.URL.Hostname()] += 1 - if t.err != nil { - return nil, t.err - } - if t.returnError != false { - req.GetBody = func() (io.ReadCloser, error) { - return nil, errHTTP - } - } - t.resp.Request = req - return t.resp, nil -} - -//////////////////////////////////////////////////////////////////////////////// - -func TestUnitAsset(t *testing.T) { - // Create a form - f := forms.SignalA_v1a{ - Value: 27.0, - } - - // Creates a single UnitAsset and assert it changes - ua := initTemplate().(*UnitAsset) - - // Change Setpt - ua.setSetPoint(f) - - if ua.Setpt != 27.0 { - t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) - } - - // Fetch Setpt w/ a form - f2 := ua.getSetPoint() - - if f2.Value != f.Value { - t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) - } -} - -func TestGetters(t *testing.T) { +func TestSetpt(t *testing.T) { ua := initTemplate().(*UnitAsset) - name := ua.GetName() - if name != "Template" { - t.Errorf("Expected name to be 2, instead got %s", name) - } - - services := ua.GetServices() - if services == nil { - t.Fatalf("Expected services not to be nil") - } - if services["setpoint"].Definition != "setpoint" { - t.Errorf("Expected definition to be setpoint") - } - - details := ua.GetDetails() - if details == nil { - t.Fatalf("Details was nil, expected map") - } - if len(details["Location"]) == 0 { - t.Fatalf("Location was nil, expected kitchen") - } - - if details["Location"][0] != "Kitchen" { - t.Errorf("Expected location to be Kitchen") - } - - cervices := ua.GetCervices() - if cervices != nil { - t.Errorf("Expected no cervices") - } -} - -func TestNewResource(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sys := components.NewSystem("testsys", ctx) - - sys.Husk = &components.Husk{ - Description: " is a controller for smart thermostats connected with a RaspBee II", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, - InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", - } - - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate - - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, _ := newResource(uac, &sys, servsTemp) - //startup() - sys.UAssets[ua.GetName()] = &ua - } -} - -type errReader int - -var errBodyRead error = fmt.Errorf("bad body read") - -func (errReader) Read(p []byte) (n int, err error) { - return 0, errBodyRead -} - -func (errReader) Close() error { - return nil -} - -func TestProcessFeedbackLoop(t *testing.T) { - // TODO: Test as much of the code as possible. - // Maybe try to pass arguments to processFeedbackLoop, to skip the GetState() function? - /* - fakeBody := // Find out what a form looks like, and pass it to this test function - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - newMockTransport(resp, false, nil) - - ua := initTemplate().(*UnitAsset) - ua.processFeedbackLoop() - */ -} - -func TestFindGateway(t *testing.T) { - fakeBody := fmt.Sprint(discoverExample) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - newMockTransport(resp, false, nil) - - // ---- All ok! ---- - err := findGateway() - - if err != nil { - t.Fatal("Gatewayn not found", err) - } - if gateway != "localhost:8080" { - t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) - } - - // ---- Error cases ---- - - // Unmarshall error - newMockTransport(resp, false, fmt.Errorf("Test error")) - err = findGateway() - if err == nil { - t.Error("Error expcted, got nil instead", err) - } - - // Statuscode > 299, have to make changes to mockTransport to test this - resp.StatusCode = 300 - newMockTransport(resp, false, nil) - err = findGateway() - if err != errStatusCode { - t.Error("Expected errStatusCode, got", err) - } - - // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body - resp.StatusCode = 200 - resp.Body = errReader(0) - newMockTransport(resp, false, nil) - err = findGateway() - if err != errBodyRead { - t.Error("Expected error") - } - - // Actual http body is unmarshaled correctly - resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) - newMockTransport(resp, false, nil) - err = findGateway() - if err == nil { - t.Error("Expected error") - } - - // Empty list of gateways - resp.Body = io.NopCloser(strings.NewReader("[]")) - newMockTransport(resp, false, nil) - err = findGateway() - if err != errMissingGateway { - t.Error("Expected error", err) - } -} - -func TestToggleState(t *testing.T) { - fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - - newMockTransport(resp, false, nil) - ua := initTemplate().(*UnitAsset) - // All ok! - ua.toggleState(true) - // Error - // change gateway to bad character/url, return gateway to original value - gateway = brokenURL - ua.toggleState(true) - findGateway() -} - -func TestSendSetPoint(t *testing.T) { - fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - - newMockTransport(resp, false, nil) - ua := initTemplate().(*UnitAsset) - // All ok! - gateway = "" - err := ua.sendSetPoint() - if err != nil { - t.Error("Unexpected error:", err) - } - - // Error - gateway = brokenURL - ua.sendSetPoint() - findGateway() - -} - -// func createRequest(data string, apiURL string) (req *http.Request, err error) -func TestCreateRequest(t *testing.T) { - data := "test" - apiURL := "http://localhost:8080/test" - - _, err := createRequest(data, apiURL) - if err != nil { - t.Error("Error occured, expected none") - } - - _, err = createRequest(data, brokenURL) - if err == nil { - t.Error("Expected error") - } - -} - -var brokenURL string = string([]byte{0x7f}) - -func TestSendRequest(t *testing.T) { - // Set up standard response & catch http requests - fakeBody := fmt.Sprint(`Test`) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - - // All ok! - newMockTransport(resp, false, nil) - apiURL := "http://localhost:8080/test" - s := fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload - req, _ := createRequest(s, apiURL) - err := sendRequest(req) - if err != nil { - t.Error("Expected no errors, error occured:", err) - } - - // Error unpacking body - resp.Body = errReader(0) - newMockTransport(resp, false, nil) - - err = sendRequest(req) - - if err == nil { - t.Error("Expected errors, no error occured:") - } - - // Error StatusCode - resp.Body = io.NopCloser(strings.NewReader(fakeBody)) - resp.StatusCode = 300 - newMockTransport(resp, false, nil) - err = sendRequest(req) - - if err != errStatusCode { - t.Error("Expected errStatusCode, got", err) + // Good case test: GET + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) + good_code := 200 + ua.setpt(w, r) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + stringBody := string(body) + // fmt.Println(stringBody) + + value := strings.Contains(string(stringBody), `"value": 20`) + unit := strings.Contains(string(stringBody), `"unit": "Celcius"`) + version := strings.Contains(string(stringBody), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_code { + t.Errorf("Good GET: Expected good status code: %v, got %v", good_code, resp.StatusCode) + } + if value != true { + t.Errorf("Good GET: The statment to be true!") + } + if unit != true { + t.Errorf("Good GET: Expected the unit statement to be true!") + } + if version != true { + t.Errorf("Good GET: Expected the version statment to be true!") + } + // Bad test case: default part of code + w = httptest.NewRecorder() + r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) + + ua.setpt(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) + } + // ALL THE ABOVE PASSES TESTS + + // Good test case: PUT + // Send PUT request to change + w = httptest.NewRecorder() + // Make the body + var of forms.SignalA_v1a + of.NewForm() + of.Value = 25.0 + of.Unit = "Celcius" + of.Timestamp = time.Now() + op, _ := usecases.Pack(&of, "application/json") + log.Println(string(op)) + sentBody := io.NopCloser(strings.NewReader(string(op))) + // Send the request + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + ua.setpt(w, r) + resp = w.Result() + good_code = 200 + // Check for errors + if resp.StatusCode != good_code { + t.Errorf("Good PUT: Expected good status code: %v, got %v", good_code, resp.StatusCode) } } From af798aa5fb6ad35f4d1cb6e07b882cdde58dc963 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 3 Feb 2025 15:41:09 +0100 Subject: [PATCH 020/102] More tests, and fixed some problems with earlier tests --- ZigBeeValve/ZigBeeValve.go | 2 -- ZigBeeValve/thing_test.go | 1 - ZigBeeValve/zigbee_test.go | 65 ++++++++++++++++++++++++++++---------- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 148741f..a444282 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -93,7 +93,6 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error processing request") http.Error(w, "Request incorrectly formated", http.StatusBadRequest) return } @@ -102,7 +101,6 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { if rsc.Model == "SmartThermostat" { err = rsc.sendSetPoint() if err != nil { - log.Println("Error sending setpoint:") http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) return } diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index 0c6f684..d6e63aa 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -158,7 +158,6 @@ func TestNewResource(t *testing.T) { ua, _ := newResource(uac, &sys, nil) // Happy test case: - //Get a unitasset with correct values name := ua.GetName() if name != "Template" { t.Errorf("Expected name to be Template, but instead got: %v", name) diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 5cb0b59..084d444 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -1,16 +1,12 @@ package main import ( + "bytes" "io" - "log" "net/http" "net/http/httptest" "strings" "testing" - "time" - - "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" ) func TestSetpt(t *testing.T) { @@ -54,28 +50,63 @@ func TestSetpt(t *testing.T) { if resp.StatusCode != http.StatusNotFound { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } - // ALL THE ABOVE PASSES TESTS - // Good test case: PUT - // Send PUT request to change + // Bad PUT (Cant reach ZigBee) w = httptest.NewRecorder() // Make the body - var of forms.SignalA_v1a - of.NewForm() - of.Value = 25.0 - of.Unit = "Celcius" - of.Timestamp = time.Now() - op, _ := usecases.Pack(&of, "application/json") - log.Println(string(op)) - sentBody := io.NopCloser(strings.NewReader(string(op))) + fakebody := string(`{"value": 24, "version": "SignalA_v1.0"}`) + sentBody := io.NopCloser(strings.NewReader(fakebody)) // Send the request r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) ua.setpt(w, r) resp = w.Result() good_code = 200 // Check for errors + if resp.StatusCode == good_code { + t.Errorf("Bad PUT: Expected bad status code: %v, got %v", good_code, resp.StatusCode) + } + + // Bad test case: PUT Failing @ HTTPProcessSetRequest + w = httptest.NewRecorder() + // Make the body + fakebody = string(`{"value": "24", "version": "SignalA_v1.0"}`) // MISSING VERSION IN SENTBODY + sentBody = io.NopCloser(strings.NewReader(fakebody)) + // Send the request + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + ua.setpt(w, r) + resp = w.Result() + // Check for errors + if resp.StatusCode == good_code { + t.Errorf("Bad PUT: Expected an error") + } + + // Good test case: PUT + w = httptest.NewRecorder() + // Make the body and request + fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) + sentBody = io.NopCloser(strings.NewReader(fakebody)) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + + // Mock the http response/traffic to zigbee + zBeeResponse := `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(zBeeResponse)), + } + newMockTransport(resp, false, nil) + // Set the response body to same as mock response + w.Body = bytes.NewBuffer([]byte(zBeeResponse)) + // Send the request + ua.setpt(w, r) + resp = w.Result() + // Check for errors if resp.StatusCode != good_code { t.Errorf("Good PUT: Expected good status code: %v, got %v", good_code, resp.StatusCode) } - + respBodyBytes, _ := io.ReadAll(resp.Body) + respBody := string(respBodyBytes) + if respBody != `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` { + t.Errorf("Wrong body") + } } From 0c663deaea370029838711ac2f628cb6e557b0ad Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 3 Feb 2025 19:08:00 +0100 Subject: [PATCH 021/102] Picks a better name for the system --- {influxdb => collector}/system.go | 6 +++--- {influxdb => collector}/unitasset.go | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) rename {influxdb => collector}/system.go (95%) rename {influxdb => collector}/unitasset.go (92%) diff --git a/influxdb/system.go b/collector/system.go similarity index 95% rename from influxdb/system.go rename to collector/system.go index c47415a..2b50ced 100644 --- a/influxdb/system.go +++ b/collector/system.go @@ -36,14 +36,14 @@ func newSystem() (sys *system) { // operations that's required of an Arrowhead system. // var sys system sys = &system{ - System: components.NewSystem("influxdb", ctx), + System: components.NewSystem("Collector", ctx), cancel: cancel, } sys.Husk = &components.Husk{ - Description: "collects data from other Arrorhead systems and sends it to a InfluxDB server.", + Description: "pulls data from other Arrorhead systems and sends it to a InfluxDB server.", Details: map[string][]string{"Developer": {"Alex"}}, ProtoPort: map[string]int{"https": 8691, "http": 8690, "coap": 0}, - InfoLink: "https://github.com/lmas/d0020e_code/tree/master/influxdb", + InfoLink: "https://github.com/lmas/d0020e_code/tree/master/collector", } return } diff --git a/influxdb/unitasset.go b/collector/unitasset.go similarity index 92% rename from influxdb/unitasset.go rename to collector/unitasset.go index d7c4598..07c88f0 100644 --- a/influxdb/unitasset.go +++ b/collector/unitasset.go @@ -10,8 +10,12 @@ import ( "github.com/sdoque/mbaigo/components" ) -// A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. +// A UnitAsset models an interface or API for a smaller part of a whole system, +// for example a single temperature sensor. // This type must implement the go interface of "components.UnitAsset" +// +// This unit asset represents this system's local cache, where all data from the +// other system are gathered, before being sent off to the InfluxDB service. type unitAsset struct { // Public fields // TODO: Why have these public and then provide getter methods? Might need refactor.. @@ -51,7 +55,7 @@ var _ components.UnitAsset = (*unitAsset)(nil) // The returned instance is used for generating the configuration file, whenever it's missing. func initTemplate() components.UnitAsset { return &unitAsset{ - Name: "InfluxDB collector", + Name: "Cache", } } From bb11160cc7ab577d4b29b3f6b1e69619649fda5f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 3 Feb 2025 20:42:49 +0100 Subject: [PATCH 022/102] issue #60: finalises the system setup --- collector/system.go | 57 ++++++++++++------- collector/unitasset.go | 126 +++++++++++++++++++++++++++++++++-------- 2 files changed, 141 insertions(+), 42 deletions(-) diff --git a/collector/system.go b/collector/system.go index 2b50ced..e7504d9 100644 --- a/collector/system.go +++ b/collector/system.go @@ -13,6 +13,16 @@ import ( func main() { sys := newSystem() sys.loadConfiguration() + + // Generate PKI keys and CSR to obtain a authentication certificate from the CA + usecases.RequestCertificate(&sys.System) + + // Register the system and its services + // WARN: this func runs a goroutine of it's own, which makes it hard to count + // using the waitgroup (and I can't be arsed to do it properly...) + usecases.RegisterServices(&sys.System) + + // Run forever sys.listenAndServe() } @@ -23,7 +33,7 @@ type system struct { components.System cancel func() - cleanups []func() + startups []func() error } func newSystem() (sys *system) { @@ -56,6 +66,8 @@ func (sys *system) loadConfiguration() { rawUAs, servsTemp, err := usecases.Configure(&sys.System) // If the file is missing, a new config will be created and an error is returned here. if err != nil { + // TODO: it would had been nice to catch the exact error for "created config.." + // and not display it as an actuall error, per se. log.Fatalf("Error while reading configuration: %v\n", err) } @@ -66,38 +78,45 @@ func (sys *system) loadConfiguration() { if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Error while unmarshalling configuration: %+v\n", err) } - ua, cleanup := newUnitAsset(uac, &sys.System, servsTemp) + ua, startup := newUnitAsset(uac, &sys.System, servsTemp) sys.UAssets[ua.GetName()] = &ua - sys.cleanups = append(sys.cleanups, cleanup) + sys.startups = append(sys.startups, startup) } } func (sys *system) listenAndServe() { var wg sync.WaitGroup // Used for counting all started goroutines - // Generate PKI keys and CSR to obtain a authentication certificate from the CA - usecases.RequestCertificate(&sys.System) - // Register the system and its services - // WARN: this func runs a goroutine of it's own, which makes it hard to count using the waitgroup (and I can't be arsed to do it properly...) - usecases.RegisterServices(&sys.System) + + // start a web server that serves basic documentation of the system + wg.Add(1) go func() { - wg.Add(1) - // start a web server and serve the request handlers in the unit assets - err := usecases.SetoutServers(&sys.System) - if err != nil { + if err := usecases.SetoutServers(&sys.System); err != nil { log.Println("Error while running web server:", err) sys.cancel() } wg.Done() }() - // Run any other goroutines here! + // Run all the startups in separate goroutines and keep track of them + for _, f := range sys.startups { + wg.Add(1) + go func(start func() error) { + if err := start(); err != nil { + log.Printf("Error while running collector: %s\n", err) + sys.cancel() + } + wg.Done() + }(f) + } + + // Block and wait for either a... + select { + case <-sys.Sigs: // user initiated shutdown signal (ctrl+c) or a... + case <-sys.Ctx.Done(): // shutdown request from a worker + } - <-sys.Sigs // Block and wait for the shutdown signal (ctrl+c) - log.Println("Shutting down system and waiting for the goroutines to terminate") - // Gracefully terminate any goroutines and wait for them to shutdown properly, before doing any cleanups + // Gracefully terminate any leftover goroutines and wait for them to shutdown properly + log.Println("Initiated shutdown, waiting for workers to terminate") sys.cancel() wg.Wait() - for _, f := range sys.cleanups { - f() - } } diff --git a/collector/unitasset.go b/collector/unitasset.go index 07c88f0..6a33286 100644 --- a/collector/unitasset.go +++ b/collector/unitasset.go @@ -4,8 +4,10 @@ package main // https://github.com/sdoque/systems/blob/main/ds18b20/thing.go import ( + "fmt" "log" "net/http" + "time" "github.com/sdoque/mbaigo/components" ) @@ -14,7 +16,7 @@ import ( // for example a single temperature sensor. // This type must implement the go interface of "components.UnitAsset" // -// This unit asset represents this system's local cache, where all data from the +// This unit asset represents a room's statistics cache, where all data from the // other system are gathered, before being sent off to the InfluxDB service. type unitAsset struct { // Public fields @@ -22,12 +24,19 @@ type unitAsset struct { Name string `json:"name"` // Must be a unique name, ie. a sensor ID Owner *components.System `json:"-"` // The parent system this UA is part of Details map[string][]string `json:"details"` // Metadata or details about this UA - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` + ServicesMap components.Services `json:"-"` // Services provided to consumers + CervicesMap components.Cervices `json:"-"` // Services being consumed // Internal fields this UA might need to perform it's function + InfluxDBHost string `json:"influxdb_host"` + InfluxDBToken string `json:"influxdb_token"` + CollectionPeriod int `json:"collection_period"` } +// Following methods are required by the interface components.UnitAsset. +// Enforce a compile-time check that the interface is implemented correctly. +var _ components.UnitAsset = (*unitAsset)(nil) + func (ua *unitAsset) GetName() string { return ua.Name } @@ -36,53 +45,124 @@ func (ua *unitAsset) GetDetails() map[string][]string { return ua.Details } -// GetServices returns all services and capabilities this UnitAsset is providing to consumers. func (ua *unitAsset) GetServices() components.Services { return ua.ServicesMap } -// GetCervices returns the list of services that is being consumed by this UnitAsset. func (ua *unitAsset) GetCervices() components.Cervices { return ua.CervicesMap } -// ensure UnitAsset implements the components.UnitAsset interface (this check is done at compile time) -var _ components.UnitAsset = (*unitAsset)(nil) +func (ua *unitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { + // We don't provide any services! + http.Error(w, "No services available", http.StatusNotImplemented) +} //////////////////////////////////////////////////////////////////////////////// +const uaName string = "Cache" + // initTemplate initializes a new UA and prefils it with some default values. // The returned instance is used for generating the configuration file, whenever it's missing. func initTemplate() components.UnitAsset { return &unitAsset{ - Name: "Cache", + Name: uaName, + Details: map[string][]string{"Location": {"Kitchen"}}, + + InfluxDBHost: "localhost:", // TODO: add port + InfluxDBToken: "insert secret token here", + CollectionPeriod: 30, } } // newUnitAsset creates a new and proper instance of UnitAsset, using settings and // values loaded from an existing configuration file. // This function returns an UA instance that is ready to be published and used, -// aswell as a function that can perform any cleanup when the system is shutting down. -func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { +// aswell as a function that can ... +// TODO: complete doc and remove servs here and in the system file +func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func() error) { + // Lost of consumed services + sProtocols := components.SProtocols(sys.Husk.ProtoPort) + temp := &components.Cervice{ + Name: "temperature", + Protos: sProtocols, + Url: make([]string, 0), + } + price := &components.Cervice{ + Name: "SEK_price", + Protos: sProtocols, + Url: make([]string, 0), + } + desired := &components.Cervice{ + Name: "desired_temp", + Protos: sProtocols, + Url: make([]string, 0), + } + setpoint := &components.Cervice{ + Name: "setpoint", + Protos: sProtocols, + Url: make([]string, 0), + } + ua := &unitAsset{ - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), + Name: uac.Name, + Owner: sys, + Details: uac.Details, + // ServicesMap: components.CloneServices(servs), // TODO: not required? + CervicesMap: components.Cervices{ + temp.Name: temp, + price.Name: price, + desired.Name: desired, + setpoint.Name: setpoint, + }, + + InfluxDBHost: uac.InfluxDBHost, + InfluxDBToken: uac.InfluxDBToken, + CollectionPeriod: uac.CollectionPeriod, + // TODO: other internal fields } + + // TODO: required for matching values with locations? + // ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) + // for _, cs := range ua.CervicesMap { + // TODO: or merge it with an empty map if this doesn't work... + // cs.Details = ua.Details + // } + // Returns the loaded unit asset and an function to handle optional cleanup at shutdown - return ua, func() { - log.Println("Cleaning up " + ua.Name) - } + return ua, ua.startup } //////////////////////////////////////////////////////////////////////////////// -// Serving maps the requested service paths with any request handlers. -func (ua *unitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { - switch servicePath { - default: - // TODO: should instead tell the visitor that there's no services published? - http.Error(w, "Invalid service request", http.StatusBadRequest) +var errTooShortPeriod error = fmt.Errorf("collection period less than 1 second") + +func (ua *unitAsset) startup() (err error) { + if ua.CollectionPeriod < 1 { + return errTooShortPeriod + } + + for { + select { + // Wait for a shutdown signal + case <-ua.Owner.Ctx.Done(): + ua.cleanup() + return + + // Wait until it's time to collect new data + case <-time.Tick(time.Duration(ua.CollectionPeriod) * time.Second): + if err = ua.collect(); err != nil { + return + } + } } } + +func (ua *unitAsset) cleanup() { + // TODO: remove this func altogether, if it's not required later on +} + +func (ua *unitAsset) collect() (err error) { + log.Println("tick") + return nil +} From a7aed2cb12f5ed20c5200c036d417a546352fad9 Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 4 Feb 2025 11:14:17 +0100 Subject: [PATCH 023/102] Fixed some merge comments --- ZigBeeValve/thing.go | 33 +++++++++-------------------- ZigBeeValve/thing_test.go | 43 +++++++++++++------------------------- ZigBeeValve/zigbee_test.go | 19 +++++++---------- 3 files changed, 32 insertions(+), 63 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 2fc9689..05b8b67 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -95,7 +95,9 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instatiate the unit assets based on configuration -// newResource creates the Resource resource with its pointers and channels based on the configuration using the tConig structs +// newResource creates the resource with its pointers and channels based on the configuration using the tConfig structs +// This is a startup function that's used to initiate the unit assets declared in the systemconfig.json, the function +// that is returned is later used to send a setpoint/start a goroutine depending on model of the unitasset func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { // deterimine the protocols that the system supports sProtocols := components.SProtocols(sys.Husk.ProtoPort) @@ -106,7 +108,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi Protos: sProtocols, Url: make([]string, 0), } - // intantiate the unit asset ua := &UnitAsset{ Name: uac.Name, @@ -121,25 +122,23 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi t.Name: t, }, } - var ref components.Service for _, s := range servs { if s.Definition == "setpoint" { ref = s } } - ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) - return ua, func() { if ua.Model == "SmartThermostat" { err := ua.sendSetPoint() if err != nil { log.Println("Error occured:", err) - // TODO: Turn off system if this startup() fails + // TODO: Turn off system if this startup() fails? } } else if ua.Model == "SmartPlug" { - // start the unit asset(s) + // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles + // between on/off depending on temperature in the room and a set temperature in the unitasset go ua.feedbackLoop(ua.Owner.Ctx) } } @@ -149,7 +148,6 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { // Initialize a ticker for periodic execution ticker := time.NewTicker(ua.Period * time.Second) defer ticker.Stop() - // start the control loop for { select { @@ -168,24 +166,22 @@ func (ua *UnitAsset) processFeedbackLoop() { log.Printf("\n unable to obtain a temperature reading error: %s\n", err) return } - // Perform a type assertion to convert the returned Form to SignalA_v1a tup, ok := tf.(*forms.SignalA_v1a) if !ok { log.Println("problem unpacking the temperature signal form") return } - // TODO: Check diff instead of a hard over/under value? meaning it'll only turn on/off if diff is over 0.5 degrees if tup.Value < ua.Setpt { err = ua.toggleState(true) if err != nil { - log.Println("Error occured: ", err) + log.Println("Error occured while toggling state to true: ", err) } } else { err = ua.toggleState(false) if err != nil { - log.Println("Error occured: ", err) + log.Println("Error occured while toggling state to false: ", err) } } } @@ -246,17 +242,11 @@ func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { // setSetPoint updates the thermal setpoint func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { ua.Setpt = f.Value - /* - log.Println("*---------------------*") - log.Printf("New set point: %.1f\n", f.Value) - log.Println("*---------------------*") - */ } func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" - // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload req, err := createRequest(s, apiURL) @@ -269,7 +259,6 @@ func (ua *UnitAsset) sendSetPoint() (err error) { func (ua *UnitAsset) toggleState(state bool) (err error) { // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" - // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload req, err := createRequest(s, apiURL) @@ -285,19 +274,17 @@ func createRequest(data string, apiURL string) (req *http.Request, err error) { if err != nil { return nil, err } - req.Header.Set("Content-Type", "application/json") // Make sure it's JSON return req, err } func sendRequest(req *http.Request) (err error) { - resp, err := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) // Perform the http request if err != nil { return err } - defer resp.Body.Close() - _, err = io.ReadAll(resp.Body) // Read the payload into body variable + _, err = io.ReadAll(resp.Body) // Read the response body, and check for errors/bad statuscodes if err != nil { return } diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index d6e63aa..f8b8956 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -72,20 +72,15 @@ func TestUnitAsset(t *testing.T) { f := forms.SignalA_v1a{ Value: 27.0, } - // Creates a single UnitAsset and assert it changes ua := initTemplate().(*UnitAsset) - // Change Setpt ua.setSetPoint(f) - if ua.Setpt != 27.0 { t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) } - // Fetch Setpt w/ a form f2 := ua.getSetPoint() - if f2.Value != f.Value { t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) } @@ -93,12 +88,12 @@ func TestUnitAsset(t *testing.T) { func TestGetters(t *testing.T) { ua := initTemplate().(*UnitAsset) - + // Test GetName() name := ua.GetName() if name != "Template" { t.Errorf("Expected name to be 2, instead got %s", name) } - + // Test GetServices() services := ua.GetServices() if services == nil { t.Fatalf("Expected services not to be nil") @@ -106,7 +101,7 @@ func TestGetters(t *testing.T) { if services["setpoint"].Definition != "setpoint" { t.Errorf("Expected definition to be setpoint") } - + // Test GetDetails() details := ua.GetDetails() if details == nil { t.Fatalf("Details was nil, expected map") @@ -114,11 +109,10 @@ func TestGetters(t *testing.T) { if len(details["Location"]) == 0 { t.Fatalf("Location was nil, expected kitchen") } - if details["Location"][0] != "Kitchen" { t.Errorf("Expected location to be Kitchen") } - + // Test GetCervices() cervices := ua.GetCervices() if cervices != nil { t.Errorf("Expected no cervices") @@ -126,6 +120,7 @@ func TestGetters(t *testing.T) { } func TestNewResource(t *testing.T) { + // Setup test context, system and unitasset ctx, cancel := context.WithCancel(context.Background()) defer cancel() sys := components.NewSystem("testsys", ctx) @@ -136,14 +131,12 @@ func TestNewResource(t *testing.T) { ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", } - setPointService := components.Service{ Definition: "setpoint", SubPath: "setpoint", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current thermal setpoint (GET) or sets it (PUT)", } - uac := UnitAsset{ Name: "Template", Details: map[string][]string{"Location": {"Kitchen"}}, @@ -155,7 +148,7 @@ func TestNewResource(t *testing.T) { setPointService.SubPath: &setPointService, }, } - + // Test newResource function ua, _ := newResource(uac, &sys, nil) // Happy test case: name := ua.GetName() @@ -176,13 +169,9 @@ func (errReader) Close() error { return nil } -func TestProcessFeedbackLoop(t *testing.T) { - // TODO: Test as much of the code as possible? -} - func TestFindGateway(t *testing.T) { + // Create mock response for findGateway function fakeBody := fmt.Sprint(discoverExample) - resp := &http.Response{ Status: "200 OK", StatusCode: 200, @@ -192,21 +181,19 @@ func TestFindGateway(t *testing.T) { // ---- All ok! ---- err := findGateway() - if err != nil { - t.Fatal("Gatewayn not found", err) + t.Fatal("Gateway not found", err) } if gateway != "localhost:8080" { t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } // ---- Error cases ---- - // Unmarshall error newMockTransport(resp, false, fmt.Errorf("Test error")) err = findGateway() if err == nil { - t.Error("Error expcted, got nil instead", err) + t.Error("Error expcted during unmarshalling, got nil instead", err) } // Statuscode > 299, have to make changes to mockTransport to test this @@ -223,7 +210,7 @@ func TestFindGateway(t *testing.T) { newMockTransport(resp, false, nil) err = findGateway() if err != errBodyRead { - t.Error("Expected error") + t.Error("Expected errBodyRead, got", err) } // Actual http body is unmarshaled correctly @@ -231,7 +218,7 @@ func TestFindGateway(t *testing.T) { newMockTransport(resp, false, nil) err = findGateway() if err == nil { - t.Error("Expected error") + t.Error("Expected error while unmarshalling body, error:", err) } // Empty list of gateways @@ -239,19 +226,18 @@ func TestFindGateway(t *testing.T) { newMockTransport(resp, false, nil) err = findGateway() if err != errMissingGateway { - t.Error("Expected error", err) + t.Error("Expected error when list of gateways is empty:", err) } } func TestToggleState(t *testing.T) { + // Create mock response and unitasset for toggleState() function fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) - resp := &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) // All ok! @@ -264,14 +250,13 @@ func TestToggleState(t *testing.T) { } func TestSendSetPoint(t *testing.T) { + // Create mock response and unitasset for sendSetPoint() function fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) - resp := &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) // All ok! diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 084d444..7f48353 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -17,11 +17,10 @@ func TestSetpt(t *testing.T) { r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) good_code := 200 ua.setpt(w, r) - + // Read response to a string, and save it in stringBody resp := w.Result() body, _ := io.ReadAll(resp.Body) stringBody := string(body) - // fmt.Println(stringBody) value := strings.Contains(string(stringBody), `"value": 20`) unit := strings.Contains(string(stringBody), `"unit": "Celcius"`) @@ -31,7 +30,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Good GET: Expected good status code: %v, got %v", good_code, resp.StatusCode) } if value != true { - t.Errorf("Good GET: The statment to be true!") + t.Errorf("Good GET: The value statment should be true!") } if unit != true { t.Errorf("Good GET: Expected the unit statement to be true!") @@ -39,14 +38,12 @@ func TestSetpt(t *testing.T) { if version != true { t.Errorf("Good GET: Expected the version statment to be true!") } - // Bad test case: default part of code + // Bad test case: Default part of code (faulty http method) w = httptest.NewRecorder() r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) - ua.setpt(w, r) - + // Read response and check statuscode, expecting 404 (StatusNotFound) resp = w.Result() - if resp.StatusCode != http.StatusNotFound { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } @@ -61,9 +58,9 @@ func TestSetpt(t *testing.T) { ua.setpt(w, r) resp = w.Result() good_code = 200 - // Check for errors + // Check for errors, should not be 200 if resp.StatusCode == good_code { - t.Errorf("Bad PUT: Expected bad status code: %v, got %v", good_code, resp.StatusCode) + t.Errorf("Bad PUT: Expected bad status code: got %v", resp.StatusCode) } // Bad test case: PUT Failing @ HTTPProcessSetRequest @@ -77,7 +74,7 @@ func TestSetpt(t *testing.T) { resp = w.Result() // Check for errors if resp.StatusCode == good_code { - t.Errorf("Bad PUT: Expected an error") + t.Errorf("Bad PUT: Expected an error during HTTPProcessSetRequest") } // Good test case: PUT @@ -86,7 +83,6 @@ func TestSetpt(t *testing.T) { fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) sentBody = io.NopCloser(strings.NewReader(fakebody)) r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) - // Mock the http response/traffic to zigbee zBeeResponse := `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` resp = &http.Response{ @@ -104,6 +100,7 @@ func TestSetpt(t *testing.T) { if resp.StatusCode != good_code { t.Errorf("Good PUT: Expected good status code: %v, got %v", good_code, resp.StatusCode) } + // Convert body to a string and check that it's correct respBodyBytes, _ := io.ReadAll(resp.Body) respBody := string(respBodyBytes) if respBody != `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` { From 7b0c41d2d954c43b335387e933ae5051b94e3127 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Feb 2025 17:44:40 +0100 Subject: [PATCH 024/102] Fixes spelling error on signal's unit --- ZigBeeValve/thing.go | 2 +- ZigBeeValve/zigbee_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 05b8b67..a52770a 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -234,7 +234,7 @@ func findGateway() (err error) { func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.Setpt - f.Unit = "Celcius" + f.Unit = "Celsius" f.Timestamp = time.Now() return f } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 7f48353..ab0f538 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -23,7 +23,7 @@ func TestSetpt(t *testing.T) { stringBody := string(body) value := strings.Contains(string(stringBody), `"value": 20`) - unit := strings.Contains(string(stringBody), `"unit": "Celcius"`) + unit := strings.Contains(string(stringBody), `"unit": "Celsius"`) version := strings.Contains(string(stringBody), `"version": "SignalA_v1.0"`) if resp.StatusCode != good_code { From b3ee0235173aa16ac3c8ff8e19149be549ee1a6d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Feb 2025 08:42:47 +0100 Subject: [PATCH 025/102] finished #60: made an initial system that is able to collect data --- collector/collect_test.go | 111 ++++++++++++++++++++++++++++ collector/system.go | 11 +-- collector/unitasset.go | 148 ++++++++++++++++++++++++++------------ go.mod | 14 +++- go.sum | 27 +++++++ 5 files changed, 261 insertions(+), 50 deletions(-) create mode 100644 collector/collect_test.go diff --git a/collector/collect_test.go b/collector/collect_test.go new file mode 100644 index 0000000..0bbb814 --- /dev/null +++ b/collector/collect_test.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "strings" + "testing" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" +) + +type mockTransport struct { + respCode int + respBody io.ReadCloser + + // hits map[string]int + // returnError bool + // resp *http.Response + // err error +} + +func newMockTransport() mockTransport { + t := mockTransport{ + respCode: 200, + respBody: io.NopCloser(strings.NewReader("")), + + // hits: make(map[string]int), + // err: err, + // returnError: retErr, + // resp: resp, + } + // Highjack the default http client so no actuall http requests are sent over the network + http.DefaultClient.Transport = t + return t +} + +func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + log.Println("HIJACK:", req.URL.String()) + // t.hits[req.URL.Hostname()] += 1 + // if t.err != nil { + // return nil, t.err + // } + // if t.returnError != false { + // req.GetBody = func() (io.ReadCloser, error) { + // return nil, errHTTP + // } + // } + // t.resp.Request = req + // return t.resp, nil + + b, err := io.ReadAll(req.Body) + if err != nil { + return + } + fmt.Println(string(b)) + + return &http.Response{ + Request: req, + StatusCode: t.respCode, + Body: t.respBody, + }, nil +} + +const mockBodyType string = "application/json" + +var mockStates = map[string]string{ + "temperature": `{ "value": 0, "unit": "Celcius", "timestamp": "%s", "version": "SignalA_v1.0" }`, + "SEK_price": `{ "value": 0.10403, "unit": "SEK", "timestamp": "%s", "version": "SignalA_v1.0" }`, + "desired_temp": `{ "value": 25, "unit": "Celsius", "timestamp": "%s", "version": "SignalA_v1.0" }`, + "setpoint": `{ "value": 20, "unit": "Celsius", "timestamp": "%s", "version": "SignalA_v1.0" }`, +} + +func mockGetState(c *components.Cervice, s *components.System) (f forms.Form, err error) { + if c == nil { + err = fmt.Errorf("got empty *Cervice instance") + return + } + b := mockStates[c.Name] + if len(b) < 1 { + err = fmt.Errorf("found no mock body for service: %s", c.Name) + return + } + body := fmt.Sprintf(b, time.Now().Format(time.RFC3339)) + f, err = usecases.Unpack([]byte(body), mockBodyType) + if err != nil { + err = fmt.Errorf("failed to unpack mock body: %s", err) + } + return +} + +func TestCollectService(t *testing.T) { + newMockTransport() + ua := newUnitAsset(*initTemplate(), newSystem(), nil) + ua.apiGetState = mockGetState + + // for _, service := range consumeServices { + // err := ua.collectService(service) + // if err != nil { + // t.Fatalf("Expected nil error while pulling %s, got: %s", service, err) + // } + // } + err := ua.collectAllServices() + if err != nil { + t.Fatalf("Expected nil error, got: %s", err) + } +} diff --git a/collector/system.go b/collector/system.go index e7504d9..f4b6031 100644 --- a/collector/system.go +++ b/collector/system.go @@ -61,7 +61,7 @@ func newSystem() (sys *system) { func (sys *system) loadConfiguration() { // Try loading the config file (in JSON format) for this deployment, // by using a unit asset with default values. - uat := initTemplate() + uat := components.UnitAsset(initTemplate()) sys.UAssets[uat.GetName()] = &uat rawUAs, servsTemp, err := usecases.Configure(&sys.System) // If the file is missing, a new config will be created and an error is returned here. @@ -78,9 +78,12 @@ func (sys *system) loadConfiguration() { if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Error while unmarshalling configuration: %+v\n", err) } - ua, startup := newUnitAsset(uac, &sys.System, servsTemp) - sys.UAssets[ua.GetName()] = &ua - sys.startups = append(sys.startups, startup) + // ua, startup := newUnitAsset(uac, &sys.System, servsTemp) + // ua := newUnitAsset(uac, &sys.System, servsTemp) + ua := newUnitAsset(uac, sys, servsTemp) + sys.startups = append(sys.startups, ua.startup) + intf := components.UnitAsset(ua) + sys.UAssets[ua.GetName()] = &intf } } diff --git a/collector/unitasset.go b/collector/unitasset.go index 6a33286..ef49709 100644 --- a/collector/unitasset.go +++ b/collector/unitasset.go @@ -7,9 +7,14 @@ import ( "fmt" "log" "net/http" + "sync" "time" + influxdb2 "github.com/influxdata/influxdb-client-go/v2" + "github.com/influxdata/influxdb-client-go/v2/api" "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" ) // A UnitAsset models an interface or API for a smaller part of a whole system, @@ -19,18 +24,24 @@ import ( // This unit asset represents a room's statistics cache, where all data from the // other system are gathered, before being sent off to the InfluxDB service. type unitAsset struct { - // Public fields - // TODO: Why have these public and then provide getter methods? Might need refactor.. + // These fields are required by the interface (below) and must also be public + // for being included in the systemconfig JSON file. Name string `json:"name"` // Must be a unique name, ie. a sensor ID Owner *components.System `json:"-"` // The parent system this UA is part of Details map[string][]string `json:"details"` // Metadata or details about this UA ServicesMap components.Services `json:"-"` // Services provided to consumers CervicesMap components.Cervices `json:"-"` // Services being consumed - // Internal fields this UA might need to perform it's function - InfluxDBHost string `json:"influxdb_host"` - InfluxDBToken string `json:"influxdb_token"` - CollectionPeriod int `json:"collection_period"` + InfluxDBHost string `json:"influxdb_host"` // IP:port addr to the influxdb server + InfluxDBToken string `json:"influxdb_token"` // Auth token + CollectionPeriod int `json:"collection_period"` // Period (in seconds) between each data collection + + // Mockable function for getting states from the consumed services. + apiGetState func(*components.Cervice, *components.System) (forms.Form, error) + + // + influx influxdb2.Client + influxWriter api.WriteAPI } // Following methods are required by the interface components.UnitAsset. @@ -64,73 +75,79 @@ const uaName string = "Cache" // initTemplate initializes a new UA and prefils it with some default values. // The returned instance is used for generating the configuration file, whenever it's missing. -func initTemplate() components.UnitAsset { +// func initTemplate() components.UnitAsset { +func initTemplate() *unitAsset { return &unitAsset{ Name: uaName, Details: map[string][]string{"Location": {"Kitchen"}}, - InfluxDBHost: "localhost:", // TODO: add port + InfluxDBHost: "http://localhost:8086", InfluxDBToken: "insert secret token here", CollectionPeriod: 30, } } +var consumeServices []string = []string{ + "temperature", + "SEK_price", + "desired_temp", + "setpoint", +} + // newUnitAsset creates a new and proper instance of UnitAsset, using settings and // values loaded from an existing configuration file. // This function returns an UA instance that is ready to be published and used, // aswell as a function that can ... // TODO: complete doc and remove servs here and in the system file -func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func() error) { - // Lost of consumed services - sProtocols := components.SProtocols(sys.Husk.ProtoPort) - temp := &components.Cervice{ - Name: "temperature", - Protos: sProtocols, - Url: make([]string, 0), - } - price := &components.Cervice{ - Name: "SEK_price", - Protos: sProtocols, - Url: make([]string, 0), - } - desired := &components.Cervice{ - Name: "desired_temp", - Protos: sProtocols, - Url: make([]string, 0), - } - setpoint := &components.Cervice{ - Name: "setpoint", - Protos: sProtocols, - Url: make([]string, 0), - } +// func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func() error) { +// func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) *unitAsset { +func newUnitAsset(uac unitAsset, sys *system, servs []components.Service) *unitAsset { + // client := influxdb2.NewClient(uac.InfluxDBHost, uac.InfluxDBToken) + ls := uint(len(consumeServices) + 1) + client := influxdb2.NewClientWithOptions( + uac.InfluxDBHost, uac.InfluxDBToken, + influxdb2.DefaultOptions().SetBatchSize(ls).SetHTTPClient(http.DefaultClient), + ) ua := &unitAsset{ Name: uac.Name, - Owner: sys, + Owner: &sys.System, Details: uac.Details, // ServicesMap: components.CloneServices(servs), // TODO: not required? - CervicesMap: components.Cervices{ - temp.Name: temp, - price.Name: price, - desired.Name: desired, - setpoint.Name: setpoint, - }, + CervicesMap: components.Cervices{}, InfluxDBHost: uac.InfluxDBHost, InfluxDBToken: uac.InfluxDBToken, CollectionPeriod: uac.CollectionPeriod, - // TODO: other internal fields + + apiGetState: usecases.GetState, + // influx: influxdb2.NewClient(uac.InfluxDBHost, uac.InfluxDBToken), + influx: client, + influxWriter: client.WriteAPI("organisation", "bucket"), + } + + // TODO: handle influx write errors or don't care? + + // Prep all the consumed services + protos := components.SProtocols(sys.Husk.ProtoPort) + for _, service := range consumeServices { + ua.CervicesMap[service] = &components.Cervice{ + Name: service, + Protos: protos, + Url: make([]string, 0), + } } // TODO: required for matching values with locations? - // ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) + // ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, nil) // for _, cs := range ua.CervicesMap { // TODO: or merge it with an empty map if this doesn't work... // cs.Details = ua.Details // } // Returns the loaded unit asset and an function to handle optional cleanup at shutdown - return ua, ua.startup + // return ua, ua.startup + return ua } //////////////////////////////////////////////////////////////////////////////// @@ -142,6 +159,8 @@ func (ua *unitAsset) startup() (err error) { return errTooShortPeriod } + // TODO: try connecting to influx, check if need to call Health()/Ping()/Ready()/Setup()? + for { select { // Wait for a shutdown signal @@ -151,7 +170,7 @@ func (ua *unitAsset) startup() (err error) { // Wait until it's time to collect new data case <-time.Tick(time.Duration(ua.CollectionPeriod) * time.Second): - if err = ua.collect(); err != nil { + if err = ua.collectAllServices(); err != nil { return } } @@ -159,10 +178,49 @@ func (ua *unitAsset) startup() (err error) { } func (ua *unitAsset) cleanup() { - // TODO: remove this func altogether, if it's not required later on + ua.influx.Close() +} + +func (ua *unitAsset) collectAllServices() (err error) { + log.Println("tick") // TODO + var wg sync.WaitGroup + + for _, service := range consumeServices { + wg.Add(1) + go func(s string) { + if err := ua.collectService(s); err != nil { + log.Printf("Error collecting data from %s: %s", s, err) + } + wg.Done() + }(service) + } + + wg.Wait() + ua.influxWriter.Flush() + return nil } -func (ua *unitAsset) collect() (err error) { - log.Println("tick") +func (ua *unitAsset) collectService(service string) (err error) { + f, err := ua.apiGetState(ua.CervicesMap[service], ua.Owner) + if err != nil { + return // TODO: use a better error? + } + // fmt.Println(f) + s, ok := f.(*forms.SignalA_v1a) + if !ok { + err = fmt.Errorf("bad form version: %s", f.FormVersion()) + return + } + fmt.Println(s) // TODO + + p := influxdb2.NewPoint( + service, + map[string]string{"unit": s.Unit}, + map[string]interface{}{"value": s.Value}, + s.Timestamp.UTC(), + ) + // fmt.Println(p) + + ua.influxWriter.WritePoint(p) return nil } diff --git a/go.mod b/go.mod index f731fe8..35962fa 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,18 @@ module github.com/lmas/d0020e_code go 1.23 -require github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 +require ( + github.com/influxdata/influxdb-client-go/v2 v2.14.0 + github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 +) +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect + github.com/oapi-codegen/runtime v1.0.0 // indirect + golang.org/x/net v0.23.0 // indirect +) + +// Replaces this library with a patched version replace github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 => github.com/lmas/mbaigo v0.0.0-20250123014631-ad869265483c diff --git a/go.sum b/go.sum index 674808d..640e94f 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,29 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4= +github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/lmas/mbaigo v0.0.0-20250123014631-ad869265483c h1:W+Jr5GQGKN4BiFOeAc6Uaq/Xc3k4/O5l+XzvsGlnlCQ= github.com/lmas/mbaigo v0.0.0-20250123014631-ad869265483c/go.mod h1:Bfx9Uj0uiTT7BCzzlImMiRd6vMoPQdsZIHGMQOVjx80= +github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= +github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= +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/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 0e3dada7914c5afaf6002aef8e573ea1af7e30f9 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Feb 2025 21:51:08 +0100 Subject: [PATCH 026/102] Adds collector and influxdb to docker-compose --- collector/system.go | 2 +- docker-compose.yml | 48 +++++++++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/collector/system.go b/collector/system.go index f4b6031..0f17b21 100644 --- a/collector/system.go +++ b/collector/system.go @@ -52,7 +52,7 @@ func newSystem() (sys *system) { sys.Husk = &components.Husk{ Description: "pulls data from other Arrorhead systems and sends it to a InfluxDB server.", Details: map[string][]string{"Developer": {"Alex"}}, - ProtoPort: map[string]int{"https": 8691, "http": 8690, "coap": 0}, + ProtoPort: map[string]int{"https": 6666, "http": 6666, "coap": 0}, InfoLink: "https://github.com/lmas/d0020e_code/tree/master/collector", } return diff --git a/docker-compose.yml b/docker-compose.yml index 19d0e8c..97a837b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,23 +83,33 @@ services: volumes: - ./data/zigbee:/data - # influxdb: - # image: influxdb:x.x.x-alpine - # ports: - # volumes: - # - ./data/influxdb:xxx - # - ## TODO: yeah gonna need a better name here - # influx: - # build: ./src/influxdb/ - # image: influx:0.2.0 - # depends_on: - # - ds18b20 - # - influxdb - # - zigbee - # - comfortstat - # ports: - # - 8870:8870 - # volumes: - # - ./data/influxdb:/data + influxdb: + image: influxdb:2.7.11-alpine + environment: + DOCKER_INFLUXDB_INIT_MODE: setup + DOCKER_INFLUXDB_INIT_USERNAME: admin + DOCKER_INFLUXDB_INIT_PASSWORD: password + DOCKER_INFLUXDB_INIT_ORG: organisation + DOCKER_INFLUXDB_INIT_BUCKET: bucket + ports: + - 8086:8086 + volumes: + - ./data/influxdb/data:/var/lib/influxdb2 + - ./data/influxdb/config:/etc/influxdb2 + + collector: + image: collector:0.1.0 + build: + context: ./src + args: + - SRC=./collector/*.go + - PORT=6666 + depends_on: + - ds18b20 + - comfortstat + - zigbee + - influxdb + network_mode: "host" + volumes: + - ./data/collector:/data From 914ced526ac826111f7821d8360a9581b8d11ad1 Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 6 Feb 2025 10:04:17 +0100 Subject: [PATCH 027/102] changed a few comments --- ZigBeeValve/zigbee_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 7f48353..e3220eb 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -12,7 +12,7 @@ import ( func TestSetpt(t *testing.T) { ua := initTemplate().(*UnitAsset) - // Good case test: GET + // --- Good case test: GET --- w := httptest.NewRecorder() r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) good_code := 200 @@ -38,7 +38,7 @@ func TestSetpt(t *testing.T) { if version != true { t.Errorf("Good GET: Expected the version statment to be true!") } - // Bad test case: Default part of code (faulty http method) + // --- Bad test case: Default part of code (faulty http method) --- w = httptest.NewRecorder() r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) ua.setpt(w, r) @@ -48,7 +48,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } - // Bad PUT (Cant reach ZigBee) + // --- Bad PUT (Cant reach ZigBee) --- w = httptest.NewRecorder() // Make the body fakebody := string(`{"value": 24, "version": "SignalA_v1.0"}`) @@ -63,7 +63,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Bad PUT: Expected bad status code: got %v", resp.StatusCode) } - // Bad test case: PUT Failing @ HTTPProcessSetRequest + // --- Bad test case: PUT Failing @ HTTPProcessSetRequest --- w = httptest.NewRecorder() // Make the body fakebody = string(`{"value": "24", "version": "SignalA_v1.0"}`) // MISSING VERSION IN SENTBODY @@ -77,7 +77,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Bad PUT: Expected an error during HTTPProcessSetRequest") } - // Good test case: PUT + // --- Good test case: PUT --- w = httptest.NewRecorder() // Make the body and request fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) From 245145ecda0c9e254ed27c17c90ff1bbb7dbf481 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 10 Feb 2025 11:03:44 +0100 Subject: [PATCH 028/102] Added functions and other goodies --- ZigBeeValve/ZigBeeValve.go | 18 ++++----- ZigBeeValve/thing.go | 80 +++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index a444282..3a134d7 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -21,11 +21,11 @@ func main() { defer cancel() // make sure all paths cancel the context to avoid context leak // instantiate the System - sys := components.NewSystem("ZigBee", ctx) + sys := components.NewSystem("ZigBeeHandler", ctx) // Instatiate the Capusle sys.Husk = &components.Husk{ - Description: " is a controller for smart thermostats connected with a RaspBee II", + Description: " is a controller for smart devices connected with a RaspBee II", Certificate: "ABCD", Details: map[string][]string{"Developer": {"Arrowhead"}}, ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, @@ -37,6 +37,12 @@ func main() { assetName := assetTemplate.GetName() sys.UAssets[assetName] = &assetTemplate + // Find zigbee gateway and store it in a global variable for reuse + err := findGateway() + if err != nil { + log.Fatal("Error getting gateway, shutting down: ", err) + } + // Configure the system rawResources, servsTemp, err := usecases.Configure(&sys) if err != nil { @@ -59,12 +65,6 @@ func main() { // Register the (system) and its services usecases.RegisterServices(&sys) - // Find zigbee gateway and store it in a global variable for reuse - err = findGateway() - if err != nil { - log.Fatal("Error getting gateway, shutting down: ", err) - } - // start the http handler and server go usecases.SetoutServers(&sys) @@ -98,7 +98,7 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { } rsc.setSetPoint(sig) - if rsc.Model == "SmartThermostat" { + if rsc.Model == "ZHAThermostat" { err = rsc.sendSetPoint() if err != nil { http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index a52770a..20310ce 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -38,10 +38,12 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Model string `json:"model"` - Period time.Duration `json:"period"` - Setpt float64 `json:"setpoint"` - Apikey string `json:"APIkey"` + Model string `json:"model"` + Uniqueid string `json:"uniqueid"` + deviceIndex string + Period time.Duration `json:"period"` + Setpt float64 `json:"setpoint"` + Apikey string `json:"APIkey"` } // GetName returns the name of the Resource. @@ -80,12 +82,14 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "Template", - Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "SmartThermostat", - Period: 10, - Setpt: 20, - Apikey: "1234", + Name: "Smart Thermostat 1", + Details: map[string][]string{"Location": {"Kitchen"}}, + Model: "SmartThermostat", + Uniqueid: "14:ef:14:10:00:6f:d0:d7-11-1201", + deviceIndex: "", + Period: 10, + Setpt: 20, + Apikey: "1234", ServicesMap: components.Services{ setPointService.SubPath: &setPointService, }, @@ -115,6 +119,8 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi Details: uac.Details, ServicesMap: components.CloneServices(servs), Model: uac.Model, + Uniqueid: uac.Uniqueid, + deviceIndex: uac.deviceIndex, Period: uac.Period, Setpt: uac.Setpt, Apikey: uac.Apikey, @@ -129,14 +135,18 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) + return ua, func() { - if ua.Model == "SmartThermostat" { + + if ua.Model == "ZHAThermostat" { + ua.getConnectedUnits("sensors") err := ua.sendSetPoint() if err != nil { - log.Println("Error occured:", err) + log.Println("Error occured during startup, while calling sendSetPoint():", err) // TODO: Turn off system if this startup() fails? } - } else if ua.Model == "SmartPlug" { + } else if ua.Model == "Smart plug" { + ua.getConnectedUnits("lights") // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles // between on/off depending on temperature in the room and a set temperature in the unitasset go ua.feedbackLoop(ua.Owner.Ctx) @@ -192,6 +202,7 @@ const discoveryURL string = "https://phoscon.de/discover" var errStatusCode error = fmt.Errorf("bad status code") var errMissingGateway error = fmt.Errorf("missing gateway") +var errMissingUniqueID error = fmt.Errorf("uniqueid not found") func findGateway() (err error) { // https://pkg.go.dev/net/http#Get @@ -246,7 +257,9 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" + + // --- Send setpoint to specific unit --- + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.deviceIndex + "/config" // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload req, err := createRequest(s, apiURL) @@ -258,7 +271,7 @@ func (ua *UnitAsset) sendSetPoint() (err error) { func (ua *UnitAsset) toggleState(state bool) (err error) { // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config - apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.deviceIndex + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload req, err := createRequest(s, apiURL) @@ -268,6 +281,40 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { return sendRequest(req) } +func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { + // Get all devices + apiURL := fmt.Sprintf("http://%s/api/%s/%s", gateway, ua.Apikey, unitType) + // Create a new request (Get) + // Put data into buffer + req, err := http.NewRequest(http.MethodGet, apiURL, nil) // Put request is made + req.Header.Set("Content-Type", "application/json") // Make sure it's JSON + // Send the request + resp, err := http.DefaultClient.Do(req) // Perform the http request + if err != nil { + return err + } + defer resp.Body.Close() + resBody, err := io.ReadAll(resp.Body) // Read the response body, and check for errors/bad statuscodes + if err != nil { + return + } + if resp.StatusCode > 299 { + return errStatusCode + } + + // How to access maps inside of maps below! + // https://stackoverflow.com/questions/28806951/accessing-nested-map-of-type-mapstringinterface-in-golang + var deviceMap map[string]interface{} + json.Unmarshal([]byte(resBody), &deviceMap) + for i := range deviceMap { + if deviceMap[i].(map[string]interface{})["uniqueid"] == ua.Uniqueid { + ua.deviceIndex = i + return + } + } + return errMissingUniqueID +} + func createRequest(data string, apiURL string) (req *http.Request, err error) { body := bytes.NewReader([]byte(data)) // Put data into buffer req, err = http.NewRequest(http.MethodPut, apiURL, body) // Put request is made @@ -293,3 +340,6 @@ func sendRequest(req *http.Request) (err error) { } return } + +// Create a group, add all lights/power plugs from e.g. kitchen to said group +// Create rule, on button.event toggle power plugs From f5b76cf129544e2f6611fb9b15b5e9a1ed421fe9 Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 11 Feb 2025 12:25:28 +0100 Subject: [PATCH 029/102] Added test for getConnectedUnits() --- ZigBeeValve/thing.go | 25 +++++--- ZigBeeValve/thing_test.go | 124 ++++++++++++++++++++++++++++++++++--- ZigBeeValve/zigbee_test.go | 34 ++++++---- 3 files changed, 154 insertions(+), 29 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 20310ce..0c78897 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -82,9 +82,9 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "Smart Thermostat 1", + Name: "SmartThermostat1", Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "SmartThermostat", + Model: "ZHAThermostat", Uniqueid: "14:ef:14:10:00:6f:d0:d7-11-1201", deviceIndex: "", Period: 10, @@ -139,14 +139,22 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi return ua, func() { if ua.Model == "ZHAThermostat" { - ua.getConnectedUnits("sensors") - err := ua.sendSetPoint() + // Get correct index in list returned by api/sensors to make sure we always change correct device + err := ua.getConnectedUnits("sensors") + if err != nil { + log.Println("Error occured during startup, while calling getConnectedUnits:", err) + } + err = ua.sendSetPoint() if err != nil { log.Println("Error occured during startup, while calling sendSetPoint():", err) // TODO: Turn off system if this startup() fails? } } else if ua.Model == "Smart plug" { - ua.getConnectedUnits("lights") + // Get correct index in list returned by api/lights to make sure we always change correct device + err := ua.getConnectedUnits("lights") + if err != nil { + log.Println("Error occured during startup, while calling getConnectedUnits:", err) + } // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles // between on/off depending on temperature in the room and a set temperature in the unitasset go ua.feedbackLoop(ua.Owner.Ctx) @@ -229,13 +237,11 @@ func findGateway() (err error) { } if len(gw) < 1 { - //log.Println("No gateway was found") return errMissingGateway } // Save the gateway s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) gateway = s - //log.Println("Gateway found:", s) return } @@ -305,7 +311,10 @@ func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { // How to access maps inside of maps below! // https://stackoverflow.com/questions/28806951/accessing-nested-map-of-type-mapstringinterface-in-golang var deviceMap map[string]interface{} - json.Unmarshal([]byte(resBody), &deviceMap) + err = json.Unmarshal([]byte(resBody), &deviceMap) + if err != nil { + return + } for i := range deviceMap { if deviceMap[i].(map[string]interface{})["uniqueid"] == ua.Uniqueid { ua.deviceIndex = i diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index f8b8956..3063f6d 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -90,8 +91,8 @@ func TestGetters(t *testing.T) { ua := initTemplate().(*UnitAsset) // Test GetName() name := ua.GetName() - if name != "Template" { - t.Errorf("Expected name to be 2, instead got %s", name) + if name != "SmartThermostat1" { + t.Errorf("Expected name to be SmartThermostat1, instead got %s", name) } // Test GetServices() services := ua.GetServices() @@ -138,9 +139,9 @@ func TestNewResource(t *testing.T) { Description: "provides the current thermal setpoint (GET) or sets it (PUT)", } uac := UnitAsset{ - Name: "Template", + Name: "SmartThermostat1", Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "SmartThermostat", + Model: "ZHAThermostat", Period: 10, Setpt: 20, Apikey: "1234", @@ -152,8 +153,8 @@ func TestNewResource(t *testing.T) { ua, _ := newResource(uac, &sys, nil) // Happy test case: name := ua.GetName() - if name != "Template" { - t.Errorf("Expected name to be Template, but instead got: %v", name) + if name != "SmartThermostat1" { + t.Errorf("Expected name to be SmartThermostat1, but instead got: %v", name) } } @@ -213,7 +214,7 @@ func TestFindGateway(t *testing.T) { t.Error("Expected errBodyRead, got", err) } - // Actual http body is unmarshaled correctly + // Actual http body is unmarshaled incorrectly resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) newMockTransport(resp, false, nil) err = findGateway() @@ -273,6 +274,106 @@ func TestSendSetPoint(t *testing.T) { gateway = "localhost" } +type testJSON struct { + FirstAttr string `json:"firstAttr"` + Uniqueid string `json:"uniqueid"` + ThirdAttr string `json:"thirdAttr"` +} + +func TestGetConnectedUnits(t *testing.T) { + gateway = "localhost" + // Set up standard response & catch http requests + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: nil, + } + ua := initTemplate().(*UnitAsset) + ua.Uniqueid = "123test" + + // --- Broken body --- + newMockTransport(resp, false, nil) + resp.Body = errReader(0) + err := ua.getConnectedUnits(ua.Model) + + if err == nil { + t.Error("Expected error while unpacking body in getConnectedUnits()") + } + + // --- All ok! --- + // Make a map + fakeBody := make(map[string]testJSON) + test := testJSON{ + FirstAttr: "123", + Uniqueid: "123test", + ThirdAttr: "456", + } + // Insert the JSON into the map with key="1" + fakeBody["1"] = test + // Marshal and create response + jsonBody, _ := json.Marshal(fakeBody) + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(string(jsonBody))), + } + // Start up a newMockTransport to capture HTTP requests before they leave + newMockTransport(resp, false, nil) + // Test function + err = ua.getConnectedUnits(ua.Model) + if err != nil { + t.Error("Expected no errors, error occured:", err) + } + + // --- Bad statuscode --- + newMockTransport(resp, false, nil) + resp.StatusCode = 300 + err = ua.getConnectedUnits(ua.Model) + if err == nil { + t.Errorf("Expected status code > 299 in getConnectedUnits(), got %v", resp.StatusCode) + } + + // --- Missing uniqueid --- + // Make a map + fakeBody = make(map[string]testJSON) + test = testJSON{ + FirstAttr: "123", + Uniqueid: "missing", + ThirdAttr: "456", + } + // Insert the JSON into the map with key="1" + fakeBody["1"] = test + // Marshal and create response + jsonBody, _ = json.Marshal(fakeBody) + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(string(jsonBody))), + } + // Start up a newMockTransport to capture HTTP requests before they leave + newMockTransport(resp, false, nil) + // Test function + err = ua.getConnectedUnits(ua.Model) + if err != errMissingUniqueID { + t.Error("Expected uniqueid to be missing when running getConnectedUnits()") + } + + // --- Unmarshall error --- + resp.Body = io.NopCloser(strings.NewReader(string(jsonBody) + "123")) + newMockTransport(resp, false, nil) + err = ua.getConnectedUnits(ua.Model) + if err == nil { + t.Error("Error expected during unmarshalling, got nil instead", err) + } + + // --- Error performing request --- + newMockTransport(resp, false, fmt.Errorf("Test error")) + err = ua.getConnectedUnits(ua.Model) + if err == nil { + t.Error("Error expected while performing http request, got nil instead") + } +} + // func createRequest(data string, apiURL string) (req *http.Request, err error) func TestCreateRequest(t *testing.T) { data := "test" @@ -313,6 +414,14 @@ func TestSendRequest(t *testing.T) { } // Break defaultClient.Do() + // --- Error performing request --- + newMockTransport(resp, false, fmt.Errorf("Test error")) + s = fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload + req, _ = createRequest(s, apiURL) + err = sendRequest(req) + if err == nil { + t.Error("Error expected while performing http request, got nil instead") + } // Error unpacking body resp.Body = errReader(0) @@ -329,7 +438,6 @@ func TestSendRequest(t *testing.T) { resp.StatusCode = 300 newMockTransport(resp, false, nil) err = sendRequest(req) - if err != errStatusCode { t.Error("Expected errStatusCode, got", err) } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 515d474..ef08958 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -11,24 +11,27 @@ import ( func TestSetpt(t *testing.T) { ua := initTemplate().(*UnitAsset) + gateway = "localhost" + ua.deviceIndex = "1" // --- Good case test: GET --- w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) + r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/SmartThermostat1/setpoint", nil) + r.Header.Set("Content-Type", "application/json") good_code := 200 ua.setpt(w, r) // Read response to a string, and save it in stringBody resp := w.Result() + if resp.StatusCode != good_code { + t.Errorf("expected good status code: %v, got %v", good_code, resp.StatusCode) + } body, _ := io.ReadAll(resp.Body) stringBody := string(body) - + // Check if correct values are present in the body, each line returns true/false value := strings.Contains(string(stringBody), `"value": 20`) unit := strings.Contains(string(stringBody), `"unit": "Celsius"`) version := strings.Contains(string(stringBody), `"version": "SignalA_v1.0"`) - - if resp.StatusCode != good_code { - t.Errorf("Good GET: Expected good status code: %v, got %v", good_code, resp.StatusCode) - } + // Check that above statements are true if value != true { t.Errorf("Good GET: The value statment should be true!") } @@ -38,9 +41,11 @@ func TestSetpt(t *testing.T) { if version != true { t.Errorf("Good GET: Expected the version statment to be true!") } + // --- Bad test case: Default part of code (faulty http method) --- w = httptest.NewRecorder() - r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) + r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/SmartThermostat1/setpoint", nil) + r.Header.Set("Content-Type", "application/json") ua.setpt(w, r) // Read response and check statuscode, expecting 404 (StatusNotFound) resp = w.Result() @@ -54,22 +59,24 @@ func TestSetpt(t *testing.T) { fakebody := string(`{"value": 24, "version": "SignalA_v1.0"}`) sentBody := io.NopCloser(strings.NewReader(fakebody)) // Send the request - r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/SmartThermostat1/setpoint", sentBody) + r.Header.Set("Content-Type", "application/json") ua.setpt(w, r) resp = w.Result() - good_code = 200 + resp.StatusCode = 404 // Simulate zigbee gateway not found? // Check for errors, should not be 200 if resp.StatusCode == good_code { - t.Errorf("Bad PUT: Expected bad status code: got %v", resp.StatusCode) + t.Errorf("Bad PUT: Expected bad status code: got %v.", resp.StatusCode) } // --- Bad test case: PUT Failing @ HTTPProcessSetRequest --- w = httptest.NewRecorder() // Make the body - fakebody = string(`{"value": "24", "version": "SignalA_v1.0"}`) // MISSING VERSION IN SENTBODY + fakebody = string(`{"value": "24"`) // MISSING VERSION IN SENTBODY sentBody = io.NopCloser(strings.NewReader(fakebody)) // Send the request - r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/SmartThermostat1/setpoint", sentBody) + r.Header.Set("Content-Type", "application/json") ua.setpt(w, r) resp = w.Result() // Check for errors @@ -82,7 +89,8 @@ func TestSetpt(t *testing.T) { // Make the body and request fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) sentBody = io.NopCloser(strings.NewReader(fakebody)) - r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/SmartThermostat1/setpoint", sentBody) + r.Header.Set("Content-Type", "application/json") // Mock the http response/traffic to zigbee zBeeResponse := `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` resp = &http.Response{ From 9a9cbae6596afd57b7fdd35b78c0567a9ec3f124 Mon Sep 17 00:00:00 2001 From: Pake Date: Wed, 12 Feb 2025 19:17:46 +0100 Subject: [PATCH 030/102] Fixed some PR comments --- ZigBeeValve/ZigBeeValve.go | 4 ++-- ZigBeeValve/thing.go | 29 ++++++++++++++++++----------- ZigBeeValve/thing_test.go | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 3a134d7..91d0d5b 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -81,7 +81,7 @@ func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath case "setpoint": t.setpt(w, r) default: - http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) + http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) } } @@ -101,11 +101,11 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { if rsc.Model == "ZHAThermostat" { err = rsc.sendSetPoint() if err != nil { + log.Println("Error sending setpoint:", err) http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) return } } - default: http.Error(w, "Method is not supported.", http.StatusNotFound) } diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 0c78897..016fd74 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -79,7 +79,14 @@ func initTemplate() components.UnitAsset { Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current thermal setpoint (GET) or sets it (PUT)", } - + /* + consumptionService := components.Service{ + Definition: "consumption", + SubPath: "consumption", + Details: map[string][]string{"Unit": {"Wh"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current consumption of the device (GET)", + } + */ // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ Name: "SmartThermostat1", @@ -137,7 +144,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) return ua, func() { - if ua.Model == "ZHAThermostat" { // Get correct index in list returned by api/sensors to make sure we always change correct device err := ua.getConnectedUnits("sensors") @@ -155,9 +161,12 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi if err != nil { log.Println("Error occured during startup, while calling getConnectedUnits:", err) } - // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles - // between on/off depending on temperature in the room and a set temperature in the unitasset - go ua.feedbackLoop(ua.Owner.Ctx) + // Not all smart plugs should be handled by the feedbackloop, some should be handled by a switch + if ua.Period != 0 { + // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles + // between on/off depending on temperature in the room and a set temperature in the unitasset + go ua.feedbackLoop(ua.Owner.Ctx) + } } } } @@ -223,7 +232,6 @@ func findGateway() (err error) { } defer res.Body.Close() if res.StatusCode > 299 { - //log.Printf("Response failed with status code: %d and\n", res.StatusCode) return errStatusCode } body, err := io.ReadAll(res.Body) // Read the payload into body variable @@ -235,7 +243,7 @@ func findGateway() (err error) { if err != nil { return } - + // If the returned list is empty, return a missing gateway error if len(gw) < 1 { return errMissingGateway } @@ -263,7 +271,6 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - // --- Send setpoint to specific unit --- apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.deviceIndex + "/config" // Create http friendly payload @@ -276,7 +283,7 @@ func (ua *UnitAsset) sendSetPoint() (err error) { } func (ua *UnitAsset) toggleState(state bool) (err error) { - // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config + // API call to toggle smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.deviceIndex + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload @@ -288,7 +295,7 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { } func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { - // Get all devices + // --- Get all devices --- apiURL := fmt.Sprintf("http://%s/api/%s/%s", gateway, ua.Apikey, unitType) // Create a new request (Get) // Put data into buffer @@ -307,7 +314,6 @@ func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { if resp.StatusCode > 299 { return errStatusCode } - // How to access maps inside of maps below! // https://stackoverflow.com/questions/28806951/accessing-nested-map-of-type-mapstringinterface-in-golang var deviceMap map[string]interface{} @@ -315,6 +321,7 @@ func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { if err != nil { return } + // --- Find the index of a device with the specific UniqueID --- for i := range deviceMap { if deviceMap[i].(map[string]interface{})["uniqueid"] == ua.Uniqueid { ua.deviceIndex = i diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index 3063f6d..e4f6aab 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -326,8 +326,8 @@ func TestGetConnectedUnits(t *testing.T) { } // --- Bad statuscode --- - newMockTransport(resp, false, nil) resp.StatusCode = 300 + newMockTransport(resp, false, nil) err = ua.getConnectedUnits(ua.Model) if err == nil { t.Errorf("Expected status code > 299 in getConnectedUnits(), got %v", resp.StatusCode) From 8f869d222ad458d57c3e7f60fde5b4baa5c4be78 Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 13 Feb 2025 03:49:13 +0100 Subject: [PATCH 031/102] Added code for a WebsocketClient, needs to be tested on Raspberry Pi --- ZigBeeValve/thing.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 016fd74..1c5969c 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -13,6 +13,8 @@ import ( "net/http" "time" + "github.com/coder/websocket" + // "github.com/coder/websocket/wsjson" "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" @@ -357,5 +359,37 @@ func sendRequest(req *http.Request) (err error) { return } -// Create a group, add all lights/power plugs from e.g. kitchen to said group -// Create rule, on button.event toggle power plugs +// --- HOW TO CONNECT AND LISTEN TO A WEBSOCKET --- +// Port 443, can be found by curl -v "http://localhost:8080/api/[apikey]/config", and getting the "websocketport". Will make a function to automatically get this port +// https://stackoverflow.com/questions/32745716/i-need-to-connect-to-an-existing-websocket-server-using-go-lang +// https://pkg.go.dev/github.com/coder/websocket#Conn +// https://pkg.go.dev/github.com/coder/websocket#Conn.Read + +// Not sure if this will work, still a work in progress. +func initWebsocketClient(ctx context.Context) (err error) { + fmt.Println("Starting Client") + ws, _, err := websocket.Dial(ctx, "ws://localhost:443", nil) // Start listening to websocket + defer ws.CloseNow() // Make sure connection is closed when returning from function + if err != nil { + fmt.Printf("Dial failed: %s\n", err) + return err + } + _, body, err := ws.Reader(ctx) // Start reading from connection, returned body will be used to get buttonevents + if err != nil { + log.Println("Error while reading from websocket:", err) + return + } + data, err := io.ReadAll(body) + if err != nil { + log.Println("Error while converthing from io.Reader to []byte:", err) + return + } + var bodyString map[string]interface{} + err = json.Unmarshal(data, &bodyString) // Unmarshal body into json, easier to be able to point to specific data with ".example" + if err != nil { + log.Println("Error while unmarshaling data:", err) + return + } + log.Println("Read from websocket:", bodyString) + return +} From 75522b07eb3f75a9ceba060d904ee5ce0ea1bcab Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 13 Feb 2025 03:51:31 +0100 Subject: [PATCH 032/102] Forgot to add go.mod & go.sum files --- go.mod | 2 ++ go.sum | 2 ++ 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index 82ef070..a6e1cec 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/lmas/d0020e_code go 1.23 require github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 + +require github.com/coder/websocket v1.8.12 // indirect diff --git a/go.sum b/go.sum index 0f4b1d6..668ac3a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 h1:feRW3hSquROFeId8H0ZEUsH/kEzd4AAVxjsYkQd1cCs= github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4/go.mod h1:Bfx9Uj0uiTT7BCzzlImMiRd6vMoPQdsZIHGMQOVjx80= From ebbb495d4a3221a8183619a792eb584eb29600ca Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 13 Feb 2025 03:58:41 +0100 Subject: [PATCH 033/102] Forgot to add a link to useful info, and a line of code to properly close connection --- ZigBeeValve/thing.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 1c5969c..2e15563 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -361,6 +361,7 @@ func sendRequest(req *http.Request) (err error) { // --- HOW TO CONNECT AND LISTEN TO A WEBSOCKET --- // Port 443, can be found by curl -v "http://localhost:8080/api/[apikey]/config", and getting the "websocketport". Will make a function to automatically get this port +// https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/ // https://stackoverflow.com/questions/32745716/i-need-to-connect-to-an-existing-websocket-server-using-go-lang // https://pkg.go.dev/github.com/coder/websocket#Conn // https://pkg.go.dev/github.com/coder/websocket#Conn.Read @@ -391,5 +392,6 @@ func initWebsocketClient(ctx context.Context) (err error) { return } log.Println("Read from websocket:", bodyString) + ws.Close(websocket.StatusNormalClosure, "No longer need to listen to websocket") return } From 51621034122fe2ce05f711258cf343fd3f5e06f4 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 14 Feb 2025 02:33:44 +0100 Subject: [PATCH 034/102] Added an error message to pass linter in example code for listening to websocket --- ZigBeeValve/thing.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 2e15563..59c337c 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -296,6 +296,8 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { return sendRequest(req) } +// Useless function? Noticed uniqueid can be used as "id" to send requests instead of the index while testing, wasn't clear from documentation. Will need to test this more though +// TODO: Rewrite this to instead get the websocketport. func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { // --- Get all devices --- apiURL := fmt.Sprintf("http://%s/api/%s/%s", gateway, ua.Apikey, unitType) @@ -363,8 +365,8 @@ func sendRequest(req *http.Request) (err error) { // Port 443, can be found by curl -v "http://localhost:8080/api/[apikey]/config", and getting the "websocketport". Will make a function to automatically get this port // https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/ // https://stackoverflow.com/questions/32745716/i-need-to-connect-to-an-existing-websocket-server-using-go-lang -// https://pkg.go.dev/github.com/coder/websocket#Conn -// https://pkg.go.dev/github.com/coder/websocket#Conn.Read +// https://pkg.go.dev/github.com/coder/websocket#Dial +// https://pkg.go.dev/github.com/coder/websocket#Conn.Reader // Not sure if this will work, still a work in progress. func initWebsocketClient(ctx context.Context) (err error) { @@ -392,6 +394,11 @@ func initWebsocketClient(ctx context.Context) (err error) { return } log.Println("Read from websocket:", bodyString) - ws.Close(websocket.StatusNormalClosure, "No longer need to listen to websocket") + err = ws.Close(websocket.StatusNormalClosure, "No longer need to listen to websocket") + if err != nil { + log.Println("Error while doing normal closure on websocket") + return + } return + // Have to do something fancy to make sure we update "connected" plugs/lights when Reader returns a body actually containing a buttonevent (something w/ channels?) } From 40042e2d83a7bcd13f83f4b1903a2eee0faf343e Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 21 Jan 2025 10:25:30 +0100 Subject: [PATCH 035/102] new branch test --- Comfortstat/Comfortstat.go | 1 + 1 file changed, 1 insertion(+) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index cd2480c..f1b51c7 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -181,4 +181,5 @@ func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { default: http.Error(w, "Method is not supported.", http.StatusNotFound) } + // new branch works!!! } From 98498b96f82389593f99c3bd83cad5e30408a823 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 22 Jan 2025 11:22:55 +0100 Subject: [PATCH 036/102] new test file added --- Comfortstat/Comfortstat_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Comfortstat/Comfortstat_test.go diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go new file mode 100644 index 0000000..71f383a --- /dev/null +++ b/Comfortstat/Comfortstat_test.go @@ -0,0 +1,11 @@ +package main + +import "testing" + +func Testtemprature(t *testing.T) { + +} + +// #1 Test if struc can handle floats +// #2 Test if we trys to update the struct, that infact the value is updated correctly +// #3 From 2d02dd0a7ec3b90c06e08ad68c935a2ad2d7c67f Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Wed, 22 Jan 2025 12:51:45 +0100 Subject: [PATCH 037/102] Corrected the calculating function --- Comfortstat/things.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 711e710..a14cfac 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -411,6 +411,7 @@ func (ua *UnitAsset) processFeedbackLoop() { maP := ua.getMax_price().Value */ //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) + ua.Desired_temp = ua.calculateDesiredTemp() // Only send temperature update when we have a new value. if ua.Desired_temp == ua.old_desired_temp { return @@ -450,9 +451,9 @@ func (ua *UnitAsset) calculateDesiredTemp() float64 { return ua.Min_temp } - k := -(ua.Max_temp - ua.Min_temp) / (ua.Max_price - ua.Min_price) - //m := max_temp - (k * min_price) + k := (ua.Min_temp - ua.Max_temp) / (ua.Max_price - ua.Min_price) + m := ua.Max_temp - (k * ua.Min_price) //m := max_temp - desired_temp := k*(ua.SEK_price-ua.Min_price) + ua.Min_temp // y - y_min = k*(x-x_min), solve for y ("desired temp") + desired_temp := k*(ua.SEK_price) + m // y - y_min = k*(x-x_min), solve for y ("desired temp") return desired_temp } From 70efeb1c4bb8a19ebc285c359d86d85ae955fd99 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Thu, 23 Jan 2025 13:42:53 +0100 Subject: [PATCH 038/102] Fixed API calls to only do it one time with several instances? --- Comfortstat/things.go | 113 ++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index a14cfac..c3db0c3 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -15,6 +15,22 @@ import ( "github.com/sdoque/mbaigo/usecases" ) +type GlobalPriceData struct { + SEK_price float64 `json:"SEK_per_kWh"` + EUR_price float64 `json:"EUR_per_kWh"` + EXR float64 `json:"EXR"` + Time_start string `json:"time_start"` + Time_end string `json:"time_end"` +} + +var globalPrice = GlobalPriceData{ + SEK_price: 0, + EUR_price: 0, + EXR: 0, + Time_start: "0", + Time_end: "0", +} + // A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. // This type must implement the go interface of "components.UnitAsset" type UnitAsset struct { @@ -46,6 +62,60 @@ type API_data struct { Time_end string `json:"time_end"` } +func priceFeedbackLoop() { + // Initialize a ticker for periodic execution + ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) + defer ticker.Stop() + + // start the control loop + for { + getAPIPriceData() + select { + case <-ticker.C: + // Block the loop until the next period + } + } +} + +func getAPIPriceData() { + url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + log.Println("URL:", url) + + res, err := http.Get(url) + if err != nil { + log.Println("Couldn't get the url, error:", err) + return + } + body, err := io.ReadAll(res.Body) // Read the payload into body variable + if err != nil { + log.Println("Something went wrong while reading the body during discovery, error:", err) + return + } + var data []GlobalPriceData // Create a list to hold the gateway json + err = json.Unmarshal(body, &data) // "unpack" body from []byte to []discoverJSON, save errors + res.Body.Close() // defer res.Body.Close() + + if res.StatusCode > 299 { + log.Printf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) + return + } + if err != nil { + log.Println("Error during Unmarshal, error:", err) + return + } + + ///////// + now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) + for _, i := range data { + if i.Time_start == now { + globalPrice.SEK_price = i.SEK_price + log.Println("Price in loop is:", i.SEK_price) + } + + } + log.Println("current el-pris is:", globalPrice.SEK_price) +} + // GetName returns the name of the Resource. func (ua *UnitAsset) GetName() string { return ua.Name @@ -114,6 +184,8 @@ func initTemplate() components.UnitAsset { Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", } + go priceFeedbackLoop() + return &UnitAsset{ // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) Name: "Set Values", @@ -324,43 +396,10 @@ func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { } func retrieveAPI_price(ua *UnitAsset) { - url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) - log.Println("URL:", url) - - res, err := http.Get(url) - if err != nil { - log.Println("Couldn't get the url, error:", err) - return - } - body, err := io.ReadAll(res.Body) // Read the payload into body variable - if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) - return - } - var data []API_data // Create a list to hold the gateway json - err = json.Unmarshal(body, &data) // "unpack" body from []byte to []discoverJSON, save errors - res.Body.Close() // defer res.Body.Close() - - if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) - return - } - if err != nil { - log.Println("Error during Unmarshal, error:", err) - return - } - - ///////// - now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) - for _, i := range data { - if i.Time_start == now { - ua.SEK_price = i.SEK_price - log.Println("Price in loop is:", i.SEK_price) - } - - } - log.Println("current el-pris is:", ua.SEK_price) - + // if globalPrice.SEK_price == 0 { + // time.Sleep(1 * time.Second) + // } + ua.SEK_price = globalPrice.SEK_price // Don't send temperature updates if the difference is too low // (this could potentially save on battery!) new_temp := ua.calculateDesiredTemp() From d5b10b90d0ac2b9132a736ee0ddb85869cf154d3 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Thu, 23 Jan 2025 14:00:19 +0100 Subject: [PATCH 039/102] Small fix to make the instances sleep a bit --- Comfortstat/things.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index c3db0c3..c5d11d7 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -396,9 +396,9 @@ func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { } func retrieveAPI_price(ua *UnitAsset) { - // if globalPrice.SEK_price == 0 { - // time.Sleep(1 * time.Second) - // } + if globalPrice.SEK_price == 0 { + time.Sleep(1 * time.Second) + } ua.SEK_price = globalPrice.SEK_price // Don't send temperature updates if the difference is too low // (this could potentially save on battery!) From 6d27800cb01839cd4988e29a5cc8b2d90bb06568 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 23 Jan 2025 16:36:29 +0100 Subject: [PATCH 040/102] more tests added --- Comfortstat/Comfortstat_test.go | 19 +++++++++-- Comfortstat/api_fetch_test.go | 56 +++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index 71f383a..b9e41db 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -1,11 +1,24 @@ package main -import "testing" +/* +func Test_structupdate(t *testing.T) { -func Testtemprature(t *testing.T) { + asset := UnitAsset{ + Min_temp: 20.0, + } + // Simulate the input signal + inputSignal := forms.SignalA_v1a{ + Value: 17.0, + } + // Call the setMin_temp function + asset.setMin_temp(inputSignal) + // Check if Min_temp is updated correctly + if asset.Min_temp != 17.0 { + t.Errorf("expected Min_temp to be 17.0, got %f", asset.Min_temp) + } } - +*/ // #1 Test if struc can handle floats // #2 Test if we trys to update the struct, that infact the value is updated correctly // #3 diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 238e1d9..553506d 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -7,10 +7,13 @@ import ( "strings" "testing" "time" + + "github.com/sdoque/mbaigo/forms" ) // mockTransport is used for replacing the default network Transport (used by // http.DefaultClient) and it will intercept network requests. + type mockTransport struct { hits map[string]int } @@ -25,6 +28,7 @@ func newMockTransport() mockTransport { } // domainHits returns the number of requests to a domain (or -1 if domain wasn't found). + func (t mockTransport) domainHits(domain string) int { for u, hits := range t.hits { if u == domain { @@ -35,17 +39,19 @@ func (t mockTransport) domainHits(domain string) int { } // TODO: this might need to be expanded to a full JSON array? + const priceExample string = `[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" -}]` + "SEK_per_kWh": 0.26673, + "EUR_per_kWh": 0.02328, + "EXR": 11.457574, + "time_start": "2025-01-06T%02d:00:00+01:00", + "time_end": "2025-01-06T%02d:00:00+01:00" + }]` // RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). // It prevents the request from being sent over the network and count how many times // a domain was requested. + func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { hour := time.Now().Local().Hour() fakeBody := fmt.Sprintf(priceExample, hour, hour+1) @@ -103,3 +109,41 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { // TODO: more test cases?? } + +func Test_structupdate_minTemp(t *testing.T) { + + asset := UnitAsset{ + Min_temp: 20.0, + } + // Simulate the input signal + Min_inputSignal := forms.SignalA_v1a{ + Value: 17.0, + } + // Call the setMin_temp function + asset.setMin_temp(Min_inputSignal) + + // check if the temprature has changed correctly + if asset.Min_temp != 17.0 { + t.Errorf("expected Min_temp to be 17.0, got %f", asset.Min_temp) + } + +} + +func Test_structupdate_maxTemp(t *testing.T) { + + asset := UnitAsset{ + Max_temp: 30.0, + } + // Simulate the input signal + inputSignal := forms.SignalA_v1a{ + Value: 21.0, + } + // Call the setMin_temp function + asset.setMax_temp(inputSignal) + + // check if the temprature has changed correctly + if asset.Min_temp != 21.0 { + t.Errorf("expected Min_temp to be 21.0, got %f", asset.Max_temp) + } + +} From 8bf2a01088722c68b5631cf4e45f1d7615e18a3a Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 23 Jan 2025 21:01:23 +0100 Subject: [PATCH 041/102] added more tests for the getters in things.go --- Comfortstat/api_fetch_test.go | 81 ++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 553506d..2a87533 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -129,21 +129,92 @@ func Test_structupdate_minTemp(t *testing.T) { } +func Test_GetTemprature(t *testing.T) { + expectedminTemp := 25.0 + expectedmaxTemp := 30.0 + expectedminPrice := 1.0 + expectedmaxPrice := 5.0 + expectedDesiredTemp := 22.5 + + uasset := UnitAsset{ + Min_temp: expectedminTemp, + Max_temp: expectedmaxTemp, + Min_price: expectedminPrice, + Max_price: expectedmaxPrice, + Desired_temp: expectedDesiredTemp, + } + //call the fuctions + result := uasset.getMin_temp() + result2 := uasset.getMax_temp() + result3 := uasset.getMin_price() + result4 := uasset.getMax_price() + result5 := uasset.getDesired_temp() + + ////MinTemp//// + // check if the value from the struct is the acctual value that the func is getting + if result.Value != expectedminTemp { + t.Errorf("expected Value to be %v, got %v", expectedminTemp, result.Value) + } + //check that the Unit is correct + if result.Unit != "Celsius" { + t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) + ////MaxTemp//// + } + if result2.Value != expectedmaxTemp { + t.Errorf("expected Value of the Min_temp is to be %v, got %v", expectedmaxTemp, result2.Value) + } + //check that the Unit is correct + if result2.Unit != "Celsius" { + t.Errorf("expected Unit of the Max_temp is to be 'Celsius', got %v", result2.Unit) + } + ////MinPrice//// + // check if the value from the struct is the acctual value that the func is getting + if result3.Value != expectedminPrice { + t.Errorf("expected Value of the maxPrice is to be %v, got %v", expectedminPrice, result3.Value) + } + //check that the Unit is correct + if result3.Unit != "SEK" { + t.Errorf("expected Unit to be 'SEK', got %v", result3.Unit) + } + + ////MaxPrice//// + // check if the value from the struct is the acctual value that the func is getting + if result4.Value != expectedmaxPrice { + t.Errorf("expected Value of the maxPrice is to be %v, got %v", expectedmaxPrice, result4.Value) + } + //check that the Unit is correct + if result4.Unit != "SEK" { + t.Errorf("expected Unit to be 'SEK', got %v", result4.Unit) + } + ////DesierdTemp//// + // check if the value from the struct is the acctual value that the func is getting + if result5.Value != expectedDesiredTemp { + t.Errorf("expected desired temprature is to be %v, got %v", expectedDesiredTemp, result5.Value) + } + //check that the Unit is correct + if result5.Unit != "Celsius" { + t.Errorf("expected Unit to be 'Celsius', got %v", result5.Unit) + } + +} + +/* func Test_structupdate_maxTemp(t *testing.T) { - asset := UnitAsset{ + asset := &UnitAsset{ Max_temp: 30.0, } // Simulate the input signal - inputSignal := forms.SignalA_v1a{ + Max_inputSignal := forms.SignalA_v1a{ Value: 21.0, } // Call the setMin_temp function - asset.setMax_temp(inputSignal) + asset.setMax_temp(Max_inputSignal) // check if the temprature has changed correctly - if asset.Min_temp != 21.0 { - t.Errorf("expected Min_temp to be 21.0, got %f", asset.Max_temp) + if asset.Max_temp != 21.0 { + t.Errorf("expected Max_temp to be 21.0, got %f", asset.Max_temp) } } +*/ From e7611ef0d0eb90a4f7ef19bc70bbe079bb16f312 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Fri, 24 Jan 2025 10:45:33 +0100 Subject: [PATCH 042/102] Fixed API to not be called in init_template --- Comfortstat/Comfortstat.go | 1 + Comfortstat/things.go | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index f1b51c7..6ecc158 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -31,6 +31,7 @@ func main() { // instantiate a template unit asset assetTemplate := initTemplate() + initAPI() assetName := assetTemplate.GetName() sys.UAssets[assetName] = &assetTemplate diff --git a/Comfortstat/things.go b/Comfortstat/things.go index c5d11d7..8875bf0 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -62,6 +62,10 @@ type API_data struct { Time_end string `json:"time_end"` } +func initAPI() { + go priceFeedbackLoop() +} + func priceFeedbackLoop() { // Initialize a ticker for periodic execution ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) @@ -184,8 +188,6 @@ func initTemplate() components.UnitAsset { Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", } - go priceFeedbackLoop() - return &UnitAsset{ // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) Name: "Set Values", From f4154a0ad8aae41797a8cb75feb822ce79c5e88d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 11:21:21 +0100 Subject: [PATCH 043/102] Installs dependencies for the tests too in the workflow --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d0f615..d95f148 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.23 + - name: Install dependencies + run: make deps - name: Run tests run: make test - name: Report stats From 0824d80d3c8596c9c2fbc863ec56e2fc3c19e6c3 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 24 Jan 2025 13:06:44 +0100 Subject: [PATCH 044/102] fixed the set-functions in thing.go and added more tests --- Comfortstat/api_fetch_test.go | 21 ++++++++++++++------- Comfortstat/things.go | 8 ++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 2a87533..d2e66a4 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -113,18 +113,23 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { func Test_structupdate_minTemp(t *testing.T) { asset := UnitAsset{ - Min_temp: 20.0, + Min_temp: 20.0, + Max_temp: 30.0, + Max_price: 10.0, + Min_price: 5.0, + SEK_price: 7.0, } // Simulate the input signal Min_inputSignal := forms.SignalA_v1a{ - Value: 17.0, + Value: 1.0, + } // Call the setMin_temp function asset.setMin_temp(Min_inputSignal) // check if the temprature has changed correctly - if asset.Min_temp != 17.0 { - t.Errorf("expected Min_temp to be 17.0, got %f", asset.Min_temp) + if asset.Min_temp != 1.0 { + t.Errorf("expected Min_temp to be 1.0, got %f", asset.Min_temp) } } @@ -198,11 +203,14 @@ func Test_GetTemprature(t *testing.T) { } -/* func Test_structupdate_maxTemp(t *testing.T) { asset := &UnitAsset{ - Max_temp: 30.0, + Min_temp: 20.0, + Max_temp: 30.0, + Max_price: 10.0, + Min_price: 5.0, + SEK_price: 7.0, } // Simulate the input signal Max_inputSignal := forms.SignalA_v1a{ @@ -217,4 +225,3 @@ func Test_structupdate_maxTemp(t *testing.T) { } } -*/ diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 8875bf0..46cabf4 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -305,7 +305,7 @@ func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { ua.Min_price = f.Value log.Printf("new minimum price: %.1f", f.Value) - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } // getMax_price is used for reading the current value of Max_price @@ -321,7 +321,7 @@ func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { ua.Max_price = f.Value log.Printf("new maximum price: %.1f", f.Value) - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } // getMin_temp is used for reading the current minimum temerature value @@ -337,7 +337,7 @@ func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { ua.Min_temp = f.Value log.Printf("new minimum temperature: %.1f", f.Value) - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } // getMax_temp is used for reading the current value of Min_price @@ -353,7 +353,7 @@ func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { ua.Max_temp = f.Value log.Printf("new maximum temperature: %.1f", f.Value) - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { From f8cf7435531d73bd826b3f0352ff118428a7da73 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 24 Jan 2025 14:08:35 +0100 Subject: [PATCH 045/102] Added working tests for getters och setters functions --- Comfortstat/api_fetch_test.go | 112 +++++++++++++++++----------------- Comfortstat/things.go | 10 +-- 2 files changed, 60 insertions(+), 62 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index d2e66a4..8589e6e 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -110,63 +110,76 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { // TODO: more test cases?? } -func Test_structupdate_minTemp(t *testing.T) { +func TestSetmethods(t *testing.T) { - asset := UnitAsset{ - Min_temp: 20.0, - Max_temp: 30.0, - Max_price: 10.0, - Min_price: 5.0, - SEK_price: 7.0, - } - // Simulate the input signal - Min_inputSignal := forms.SignalA_v1a{ + asset := initTemplate().(*UnitAsset) + + // Simulate the input signals + MinTemp_inputSignal := forms.SignalA_v1a{ Value: 1.0, - } + MaxTemp_inputSignal := forms.SignalA_v1a{ + Value: 29.0, + } + MinPrice_inputSignal := forms.SignalA_v1a{ + Value: 2.0, + } + MaxPrice_inputSignal := forms.SignalA_v1a{ + Value: 12.0, + } + DesTemp_inputSignal := forms.SignalA_v1a{ + Value: 23.7, + } + // Call the setMin_temp function - asset.setMin_temp(Min_inputSignal) + asset.setMin_temp(MinTemp_inputSignal) + asset.setMax_temp(MaxTemp_inputSignal) + asset.setMin_price(MinPrice_inputSignal) + asset.setMax_price(MaxPrice_inputSignal) + asset.setDesired_temp(DesTemp_inputSignal) // check if the temprature has changed correctly if asset.Min_temp != 1.0 { t.Errorf("expected Min_temp to be 1.0, got %f", asset.Min_temp) } + if asset.Max_temp != 29.0 { + t.Errorf("expected Max_temp to be 25.0, got %f", asset.Max_temp) + } + if asset.Min_price != 2.0 { + t.Errorf("expected Min_Price to be 2.0, got %f", asset.Min_price) + } + if asset.Max_price != 12.0 { + t.Errorf("expected Max_Price to be 12.0, got %f", asset.Max_price) + } + if asset.Desired_temp != 23.7 { + t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.Desired_temp) + } } -func Test_GetTemprature(t *testing.T) { - expectedminTemp := 25.0 - expectedmaxTemp := 30.0 - expectedminPrice := 1.0 - expectedmaxPrice := 5.0 - expectedDesiredTemp := 22.5 +func Test_GetMethods(t *testing.T) { - uasset := UnitAsset{ - Min_temp: expectedminTemp, - Max_temp: expectedmaxTemp, - Min_price: expectedminPrice, - Max_price: expectedmaxPrice, - Desired_temp: expectedDesiredTemp, - } + uasset := initTemplate().(*UnitAsset) //call the fuctions result := uasset.getMin_temp() result2 := uasset.getMax_temp() result3 := uasset.getMin_price() result4 := uasset.getMax_price() result5 := uasset.getDesired_temp() + result6 := uasset.getSEK_price() ////MinTemp//// // check if the value from the struct is the acctual value that the func is getting - if result.Value != expectedminTemp { - t.Errorf("expected Value to be %v, got %v", expectedminTemp, result.Value) + if result.Value != uasset.Min_temp { + t.Errorf("expected Value of the min_temp is to be %v, got %v", uasset.Min_temp, result.Value) } //check that the Unit is correct if result.Unit != "Celsius" { t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) ////MaxTemp//// } - if result2.Value != expectedmaxTemp { - t.Errorf("expected Value of the Min_temp is to be %v, got %v", expectedmaxTemp, result2.Value) + if result2.Value != uasset.Max_temp { + t.Errorf("expected Value of the Max_temp is to be %v, got %v", uasset.Max_temp, result2.Value) } //check that the Unit is correct if result2.Unit != "Celsius" { @@ -174,8 +187,8 @@ func Test_GetTemprature(t *testing.T) { } ////MinPrice//// // check if the value from the struct is the acctual value that the func is getting - if result3.Value != expectedminPrice { - t.Errorf("expected Value of the maxPrice is to be %v, got %v", expectedminPrice, result3.Value) + if result3.Value != uasset.Min_price { + t.Errorf("expected Value of the minPrice is to be %v, got %v", uasset.Min_price, result3.Value) } //check that the Unit is correct if result3.Unit != "SEK" { @@ -184,8 +197,8 @@ func Test_GetTemprature(t *testing.T) { ////MaxPrice//// // check if the value from the struct is the acctual value that the func is getting - if result4.Value != expectedmaxPrice { - t.Errorf("expected Value of the maxPrice is to be %v, got %v", expectedmaxPrice, result4.Value) + if result4.Value != uasset.Max_price { + t.Errorf("expected Value of the maxPrice is to be %v, got %v", uasset.Max_price, result4.Value) } //check that the Unit is correct if result4.Unit != "SEK" { @@ -193,35 +206,20 @@ func Test_GetTemprature(t *testing.T) { } ////DesierdTemp//// // check if the value from the struct is the acctual value that the func is getting - if result5.Value != expectedDesiredTemp { - t.Errorf("expected desired temprature is to be %v, got %v", expectedDesiredTemp, result5.Value) + if result5.Value != uasset.Desired_temp { + t.Errorf("expected desired temprature is to be %v, got %v", uasset.Desired_temp, result5.Value) } //check that the Unit is correct if result5.Unit != "Celsius" { t.Errorf("expected Unit to be 'Celsius', got %v", result5.Unit) } - -} - -func Test_structupdate_maxTemp(t *testing.T) { - - asset := &UnitAsset{ - Min_temp: 20.0, - Max_temp: 30.0, - Max_price: 10.0, - Min_price: 5.0, - SEK_price: 7.0, - } - // Simulate the input signal - Max_inputSignal := forms.SignalA_v1a{ - Value: 21.0, - } - // Call the setMin_temp function - asset.setMax_temp(Max_inputSignal) - - // check if the temprature has changed correctly - if asset.Max_temp != 21.0 { - t.Errorf("expected Max_temp to be 21.0, got %f", asset.Max_temp) + ////SEK_Price//// + if result6.Value != uasset.SEK_price { + t.Errorf("expected electric price is to be %v, got %v", uasset.SEK_price, result6.Value) } + //check that the Unit is correct + //if result5.Unit != "SEK" { + // t.Errorf("expected Unit to be 'SEK', got %v", result6.Unit) + //} } diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 46cabf4..ca92f75 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -286,7 +286,7 @@ func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { // setSEK_price updates the current electric price with the new current electric hourly price func (ua *UnitAsset) setSEK_price(f forms.SignalA_v1a) { ua.SEK_price = f.Value - log.Printf("new electric price: %.1f", f.Value) + //log.Printf("new electric price: %.1f", f.Value) } ///////////////////////////////////////////////////////////////////////// @@ -304,7 +304,7 @@ func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { // setMin_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { ua.Min_price = f.Value - log.Printf("new minimum price: %.1f", f.Value) + //log.Printf("new minimum price: %.1f", f.Value) //ua.processFeedbackLoop() } @@ -320,7 +320,7 @@ func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { // setMax_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { ua.Max_price = f.Value - log.Printf("new maximum price: %.1f", f.Value) + //log.Printf("new maximum price: %.1f", f.Value) //ua.processFeedbackLoop() } @@ -336,7 +336,7 @@ func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { // setMin_temp updates the current minimum temperature set by the user with a new value func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { ua.Min_temp = f.Value - log.Printf("new minimum temperature: %.1f", f.Value) + //log.Printf("new minimum temperature: %.1f", f.Value) //ua.processFeedbackLoop() } @@ -352,7 +352,7 @@ func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { // setMax_temp updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { ua.Max_temp = f.Value - log.Printf("new maximum temperature: %.1f", f.Value) + //log.Printf("new maximum temperature: %.1f", f.Value) //ua.processFeedbackLoop() } From 47bb06e05d4a3e38d0b61e9b3fbb2e1d03a5edea Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 24 Jan 2025 16:05:51 +0100 Subject: [PATCH 046/102] added some more tests --- Comfortstat/api_fetch_test.go | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 8589e6e..807113f 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -223,3 +223,48 @@ func Test_GetMethods(t *testing.T) { //} } + +func Test_initTemplet(t *testing.T) { + uasset := initTemplate().(*UnitAsset) + + name := uasset.GetName() + Services := uasset.GetServices() + //Cervices := uasset.GetCervices() + Details := uasset.GetDetails() + + //// unnecessary test, but good for practicing + if name != "Set Values" { + t.Errorf("expected name of the resource is %v, got %v", uasset.Name, name) + } + if Services == nil { + t.Fatalf("If Services is nil, not worth to continue testing") + } + ////Services//// + if Services["SEK_price"].Definition != "SEK_price" { + t.Errorf("expected service defenition to be SEKprice") + } + if Services["max_temperature"].Definition != "max_temperature" { + t.Errorf("expected service defenition to be max_temperature") + } + if Services["min_temperature"].Definition != "min_temperature" { + t.Errorf("expected service defenition to be min_temperature") + } + if Services["max_price"].Definition != "max_price" { + t.Errorf("expected service defenition to be max_price") + } + if Services["min_price"].Definition != "min_price" { + t.Errorf("expected service defenition to be min_price") + } + if Services["desired_temp"].Definition != "desired_temp" { + t.Errorf("expected service defenition to be desired_temp") + } + //// Testing GetCervice + //if Cervices == nil { + // t.Fatalf("If cervises is nil, not worth to continue testing") + //} + //// Testing Details + if Details == nil { + t.Errorf("expected a map, but Details was nil, ") + } + +} From 052a3317376b12680ab73468d87b650ee893c492 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 28 Jan 2025 09:15:03 +0100 Subject: [PATCH 047/102] adding tests plus cleaning up things.go --- Comfortstat/api_fetch_test.go | 61 +++++++++++++++++++++++++++++++++++ Comfortstat/things.go | 26 +++------------ 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 807113f..c1067fb 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -1,14 +1,19 @@ package main import ( + "context" + "encoding/json" "fmt" "io" + "log" "net/http" "strings" "testing" "time" + "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" ) // mockTransport is used for replacing the default network Transport (used by @@ -268,3 +273,59 @@ func Test_initTemplet(t *testing.T) { } } + +func Test_newUnitAsset(t *testing.T) { + // prepare for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled + defer cancel() // make sure all paths cancel the context to avoid context leak + + // instantiate the System + sys := components.NewSystem("Comfortstat", ctx) + + // Instatiate the Capusle + sys.Husk = &components.Husk{ + Description: " is a controller for a consumed servo motor position based on a consumed temperature", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, + InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + //initAPI() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + + // Configure the system + rawResources, servsTemp, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) + } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac UnitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, cleanup := newUnitAsset(uac, &sys, servsTemp) + defer cleanup() + sys.UAssets[ua.GetName()] = &ua + } + + // Skriv if-satser som kollar namn och services + // testa calculatedeiserdTemp(nytt test) + // processfeedbackloop(nytt test) + // +} + +func Test_calculateDesiredTemp(t *testing.T) { + var True_result float64 = 22.5 + asset := initTemplate().(*UnitAsset) + + result := asset.calculateDesiredTemp() + + if result != True_result { + t.Errorf("Expected calculated temp is %v, got %v", True_result, result) + } +} diff --git a/Comfortstat/things.go b/Comfortstat/things.go index ca92f75..db64955 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -192,9 +192,9 @@ func initTemplate() components.UnitAsset { // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) Name: "Set Values", Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 7.5, // Example electricity price in SEK per kWh - Min_price: 0.0, // Minimum price allowed - Max_price: 0.02, // Maximum price allowed + SEK_price: 1.5, // Example electricity price in SEK per kWh + Min_price: 1.0, // Minimum price allowed + Max_price: 2.0, // Maximum price allowed Min_temp: 20.0, // Minimum temperature Max_temp: 25.0, // Maximum temprature allowed Desired_temp: 0, // Desired temp calculated by system @@ -432,25 +432,7 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { func (ua *UnitAsset) processFeedbackLoop() { // get the current temperature - /* - tf, err := usecases.GetState(ua.CervicesMap["setpoint"], ua.Owner) - if err != nil { - log.Printf("\n unable to obtain a setpoint reading error: %s\n", err) - return - } - // Perform a type assertion to convert the returned Form to SignalA_v1a - tup, ok := tf.(*forms.SignalA_v1a) - if !ok { - log.Println("problem unpacking the setpoint signal form") - return - } - */ - /* - miT := ua.getMin_temp().Value - maT := ua.getMax_temp().Value - miP := ua.getMin_price().Value - maP := ua.getMax_price().Value - */ + //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) ua.Desired_temp = ua.calculateDesiredTemp() // Only send temperature update when we have a new value. From e1070be2c61bf4b7114be884c618e42a5822f367 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 28 Jan 2025 10:51:37 +0100 Subject: [PATCH 048/102] trying to test processfeedbackLoop --- Comfortstat/api_fetch_test.go | 59 +++++++++++++++++++++++++++++++++++ Comfortstat/things.go | 4 ++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index c1067fb..9656dfa 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -329,3 +329,62 @@ func Test_calculateDesiredTemp(t *testing.T) { t.Errorf("Expected calculated temp is %v, got %v", True_result, result) } } + +func Test_specialcalculate(t *testing.T) { + asset := UnitAsset{ + SEK_price: 3.0, + Max_price: 2.0, + Min_temp: 17.0, + } + + result := asset.calculateDesiredTemp() + + if result != asset.Min_temp { + t.Errorf("Expected temperature to be %v, got %v", asset.Min_temp, result) + } +} + +// Define a simple implementation for usecases.Pack and usecases.SetState +func dummyPack(data interface{}, contentType string) ([]byte, error) { + // Simulate successful packing of the data + return []byte("dummy-packed-data"), nil +} + +func dummySetState(service interface{}, owner string, data []byte) error { + // Simulate successful state setting + return nil +} + +func Test_processFeedbackLoop(t *testing.T) { + + unit := initTemplate().(*UnitAsset) + // Create a sample UnitAsset with necessary fields initialized + /* + unit := UnitAsset{ + Desired_temp: 20.0, // Initial desired temperature + old_desired_temp: 15.0, + CervicesMap: map[string]Service{ + "setpoint": { + Details: map[string][]string{ + "Unit": {"C"}, + }, + }, + }, + Owner: "TestOwner", + } + */ + + // Replace usecases.Pack and usecases.SetState with dummy implementations + usecases.Pack = dummyPack + usecases.SetState = dummySetState + + // Run the processFeedbackLoop method + unit.processFeedbackLoop() + + // Verify the results + if unit.old_desired_temp != unit.Desired_temp { + t.Errorf("Expected old_desired_temp to be updated to %v, got %v", unit.Desired_temp, unit.old_desired_temp) + } + + // Add more assertions as needed, such as checking if dummySetState was called +} diff --git a/Comfortstat/things.go b/Comfortstat/things.go index db64955..c321424 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -283,12 +283,13 @@ func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { return f } +/* // setSEK_price updates the current electric price with the new current electric hourly price func (ua *UnitAsset) setSEK_price(f forms.SignalA_v1a) { ua.SEK_price = f.Value //log.Printf("new electric price: %.1f", f.Value) } - +*/ ///////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////// @@ -454,6 +455,7 @@ func (ua *UnitAsset) processFeedbackLoop() { of.Timestamp = time.Now() // pack the new valve state form + // Pack() converting the data in "of" into JSON format op, err := usecases.Pack(&of, "application/json") if err != nil { return From 019468327915f6538d82f93306be71d175629bd9 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 28 Jan 2025 11:29:56 +0100 Subject: [PATCH 049/102] trying to test processfeedbackLoop --- Comfortstat/api_fetch_test.go | 75 +++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 9656dfa..73d0026 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -344,47 +344,52 @@ func Test_specialcalculate(t *testing.T) { } } -// Define a simple implementation for usecases.Pack and usecases.SetState -func dummyPack(data interface{}, contentType string) ([]byte, error) { - // Simulate successful packing of the data - return []byte("dummy-packed-data"), nil -} +func Test_processfeedbackLoop(t *testing.T) { + ua := initTemplate().(*UnitAsset) -func dummySetState(service interface{}, owner string, data []byte) error { - // Simulate successful state setting - return nil -} + // Set the calculateDesiredTemp function to simulate a new temperature value + ua.calculateDesiredTemp = func() float64 { + return 23.0 // Just return a new temp value to trigger a change + } -func Test_processFeedbackLoop(t *testing.T) { - - unit := initTemplate().(*UnitAsset) - // Create a sample UnitAsset with necessary fields initialized - /* - unit := UnitAsset{ - Desired_temp: 20.0, // Initial desired temperature - old_desired_temp: 15.0, - CervicesMap: map[string]Service{ - "setpoint": { - Details: map[string][]string{ - "Unit": {"C"}, - }, - }, - }, - Owner: "TestOwner", - } - */ + // Override the Pack function to simulate no error and return dummy data + usecases.Pack = func(form *forms.SignalA_v1a, contentType string) ([]byte, error) { + return []byte("packed data"), nil + } + + // Override the SetState function to simulate a successful update + usecases.SetState = func(setpoint interface{}, owner interface{}, op []byte) error { + return nil + } - // Replace usecases.Pack and usecases.SetState with dummy implementations - usecases.Pack = dummyPack - usecases.SetState = dummySetState + // Create a variable to hold the SignalA_v1a form to compare later + // Set the form's value, unit, and timestamp to simulate what the method does + var of forms.SignalA_v1a + of.NewForm() + of.Value = ua.Desired_temp + of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] // This matches the code that fetches the "Unit" + of.Timestamp = time.Now() // Run the processFeedbackLoop method - unit.processFeedbackLoop() + ua.processFeedbackLoop() + + // Check if the Desired_temp was updated + if ua.Desired_temp != 23.0 { + t.Errorf("Expected Desired_temp to be 23.0, but got %f", ua.Desired_temp) + } + + // Check if the old_desired_temp was updated + if ua.old_desired_temp != 23.0 { + t.Errorf("Expected old_desired_temp to be 23.0, but got %f", ua.old_desired_temp) + } + + // Optionally, check if the values in the form match what was expected + if of.Value != ua.Desired_temp { + t.Errorf("Expected form Value to be %f, but got %f", ua.Desired_temp, of.Value) + } - // Verify the results - if unit.old_desired_temp != unit.Desired_temp { - t.Errorf("Expected old_desired_temp to be updated to %v, got %v", unit.Desired_temp, unit.old_desired_temp) + if of.Unit != "Celsius" { + t.Errorf("Expected form Unit to be 'Celsius', but got '%s'", of.Unit) } - // Add more assertions as needed, such as checking if dummySetState was called } From b043b428294e2c072bc4ccda5a9ad7a1968a66b2 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 11:24:50 +0100 Subject: [PATCH 050/102] cleand up some things --- Comfortstat/things.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index c321424..525c561 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -34,8 +34,6 @@ var globalPrice = GlobalPriceData{ // A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. // This type must implement the go interface of "components.UnitAsset" type UnitAsset struct { - // Public fields - // TODO: Why have these public and then provide getter methods? Might need refactor.. Name string `json:"name"` // Must be a unique name, ie. a sensor ID Owner *components.System `json:"-"` // The parent system this UA is part of Details map[string][]string `json:"details"` // Metadata or details about this UA @@ -96,7 +94,7 @@ func getAPIPriceData() { return } var data []GlobalPriceData // Create a list to hold the gateway json - err = json.Unmarshal(body, &data) // "unpack" body from []byte to []discoverJSON, save errors + err = json.Unmarshal(body, &data) // "unpack" body from []byte to []GlobalPriceData, save errors res.Body.Close() // defer res.Body.Close() if res.StatusCode > 299 { From db36b3b366c4f3167f70658f24512f1aeb055e78 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 11:25:38 +0100 Subject: [PATCH 051/102] working in GetapiPrice test --- Comfortstat/api_fetch_test.go | 110 ++++++++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 25 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 73d0026..26fd2bf 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "net/http/httptest" "strings" "testing" "time" @@ -344,34 +345,36 @@ func Test_specialcalculate(t *testing.T) { } } +/* func Test_processfeedbackLoop(t *testing.T) { ua := initTemplate().(*UnitAsset) - // Set the calculateDesiredTemp function to simulate a new temperature value - ua.calculateDesiredTemp = func() float64 { - return 23.0 // Just return a new temp value to trigger a change - } - // Override the Pack function to simulate no error and return dummy data - usecases.Pack = func(form *forms.SignalA_v1a, contentType string) ([]byte, error) { - return []byte("packed data"), nil - } + // Set the calculateDesiredTemp function to simulate a new temperature value + ua.calculateDesiredTemp = func() float64 { + return 23.0 // Just return a new temp value to trigger a change + } - // Override the SetState function to simulate a successful update - usecases.SetState = func(setpoint interface{}, owner interface{}, op []byte) error { - return nil - } + // Override the Pack function to simulate no error and return dummy data + usecases.Pack = func(form *forms.SignalA_v1a, contentType string) ([]byte, error) { + return []byte("packed data"), nil + } + + // Override the SetState function to simulate a successful update + usecases.SetState = func(setpoint interface{}, owner interface{}, op []byte) error { + return nil + } - // Create a variable to hold the SignalA_v1a form to compare later - // Set the form's value, unit, and timestamp to simulate what the method does - var of forms.SignalA_v1a - of.NewForm() - of.Value = ua.Desired_temp - of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] // This matches the code that fetches the "Unit" - of.Timestamp = time.Now() + // Create a variable to hold the SignalA_v1a form to compare later + // Set the form's value, unit, and timestamp to simulate what the method does + var of forms.SignalA_v1a + of.NewForm() + of.Value = ua.Desired_temp + of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] // This matches the code that fetches the "Unit" + of.Timestamp = time.Now() // Run the processFeedbackLoop method - ua.processFeedbackLoop() + //ua.processFeedbackLoop() // Check if the Desired_temp was updated if ua.Desired_temp != 23.0 { @@ -383,13 +386,70 @@ func Test_processfeedbackLoop(t *testing.T) { t.Errorf("Expected old_desired_temp to be 23.0, but got %f", ua.old_desired_temp) } - // Optionally, check if the values in the form match what was expected - if of.Value != ua.Desired_temp { - t.Errorf("Expected form Value to be %f, but got %f", ua.Desired_temp, of.Value) + + // Optionally, check if the values in the form match what was expected + if of.Value != ua.Desired_temp { + t.Errorf("Expected form Value to be %f, but got %f", ua.Desired_temp, of.Value) + } + + if of.Unit != "Celsius" { + t.Errorf("Expected form Unit to be 'Celsius', but got '%s'", of.Unit) + } + +} +*/ +// Custom RoundTripper to intercept HTTP requests +type MockTransport struct { + mockServerURL string +} + +// Implement the RoundTrip function for MockTransport +func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Modify the request to point to our mock server + req.URL.Scheme = "http" + req.URL.Host = m.mockServerURL[len("http://"):] // Remove "http://" + return http.DefaultTransport.RoundTrip(req) +} + +func TestGetAPIPriceData(t *testing.T) { + + // Create mock response + fakebody := []GlobalPriceData{ + { + Time_start: fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, + time.Now().Local().Year(), + int(time.Now().Local().Month()), + time.Now().Local().Day(), + time.Now().Local().Hour()), + + SEK_price: 1.23, + }, } + mockData, _ := json.Marshal(fakebody) + + // Start a mock HTTP server + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) // this simulated a succesfull response (status 2000) + w.Write(mockData) + })) + defer mockServer.Close() - if of.Unit != "Celsius" { - t.Errorf("Expected form Unit to be 'Celsius', but got '%s'", of.Unit) + // Override the default HTTP client with our mock transport + client := &http.Client{ + Transport: &MockTransport{mockServerURL: mockServer.URL}, } + // Temporarily replace the global HTTP client + originalClient := http.DefaultClient + http.DefaultClient = client + defer func() { http.DefaultClient = originalClient }() // Restore after test + + // Call the function (which now hits the mock server) + getAPIPriceData() + + // Check if the correct price is stored + expectedPrice := 1.23 + if globalPrice.SEK_price != expectedPrice { + t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) + } } From 2ae7a321ac1589a4e65602e9da67d34c6453ee6a Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 12:04:14 +0100 Subject: [PATCH 052/102] cleaned up some comments and added some comments to parts with no explanation --- Comfortstat/things.go | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 525c561..1761007 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -156,9 +156,9 @@ func initTemplate() components.UnitAsset { } setMax_temp := components.Service{ - Definition: "max_temperature", // TODO: this get's incorrectly linked to the below subpath - SubPath: "max_temperature", // TODO: this path needs to be setup in Serving() too - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, // TODO: why this form here?? + Definition: "max_temperature", + SubPath: "max_temperature", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the maximum temp the user wants (using a GET request)", } setMin_temp := components.Service{ @@ -198,7 +198,7 @@ func initTemplate() components.UnitAsset { Desired_temp: 0, // Desired temp calculated by system Period: 15, - // Don't forget to map the provided services from above! + // maps the provided services from above ServicesMap: components.Services{ setMax_temp.SubPath: &setMax_temp, setMin_temp.SubPath: &setMin_temp, @@ -220,7 +220,7 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv sProtocol := components.SProtocols(sys.Husk.ProtoPort) - // the Cervice that is to be consumed by zigbee, there fore the name with the C + // the Cervice that is to be consumed by zigbee, therefore the name with the C t := &components.Cervice{ Name: "setpoint", @@ -255,8 +255,6 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv ua.CervicesMap["setpoint"].Details = components.MergeDetails(ua.Details, ref.Details) - // ua.CervicesMap["setPoint"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}) - // start the unit asset(s) go ua.feedbackLoop(sys.Ctx) go ua.API_feedbackLoop(sys.Ctx) @@ -303,8 +301,6 @@ func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { // setMin_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { ua.Min_price = f.Value - //log.Printf("new minimum price: %.1f", f.Value) - //ua.processFeedbackLoop() } // getMax_price is used for reading the current value of Max_price @@ -319,8 +315,6 @@ func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { // setMax_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { ua.Max_price = f.Value - //log.Printf("new maximum price: %.1f", f.Value) - //ua.processFeedbackLoop() } // getMin_temp is used for reading the current minimum temerature value @@ -335,8 +329,6 @@ func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { // setMin_temp updates the current minimum temperature set by the user with a new value func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { ua.Min_temp = f.Value - //log.Printf("new minimum temperature: %.1f", f.Value) - //ua.processFeedbackLoop() } // getMax_temp is used for reading the current value of Min_price @@ -351,8 +343,6 @@ func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { // setMax_temp updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { ua.Max_temp = f.Value - //log.Printf("new maximum temperature: %.1f", f.Value) - //ua.processFeedbackLoop() } func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { @@ -430,7 +420,7 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { // func (ua *UnitAsset) processFeedbackLoop() { - // get the current temperature + // get the current best temperature //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) ua.Desired_temp = ua.calculateDesiredTemp() @@ -441,10 +431,6 @@ func (ua *UnitAsset) processFeedbackLoop() { // Keep track of previous value ua.old_desired_temp = ua.Desired_temp - // perform the control algorithm - // ua.deviation = ua.Setpt - tup.Value - // output := ua.calculateOutput(ua.deviation) - // prepare the form to send var of forms.SignalA_v1a of.NewForm() @@ -466,7 +452,10 @@ func (ua *UnitAsset) processFeedbackLoop() { } } +// Calculates the new most optimal temprature (desierdTemp) based on the price/temprature intervalls +// and the current electricity price func (ua *UnitAsset) calculateDesiredTemp() float64 { + if ua.SEK_price <= ua.Min_price { return ua.Max_temp } @@ -476,7 +465,7 @@ func (ua *UnitAsset) calculateDesiredTemp() float64 { k := (ua.Min_temp - ua.Max_temp) / (ua.Max_price - ua.Min_price) m := ua.Max_temp - (k * ua.Min_price) - //m := max_temp - desired_temp := k*(ua.SEK_price) + m // y - y_min = k*(x-x_min), solve for y ("desired temp") + desired_temp := k*(ua.SEK_price) + m + return desired_temp } From 4e085bca5796f3d53626f79c86a8db75010ab710 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 16:22:59 +0100 Subject: [PATCH 053/102] More tests --- Comfortstat/api_fetch_test.go | 71 +++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 26fd2bf..3c32e70 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -403,34 +403,75 @@ type MockTransport struct { mockServerURL string } -// Implement the RoundTrip function for MockTransport +// Implement the RoundTrip function for MockTransport, here is where the logic on how HTTP request are handled +// modify the request to point at the created mock server func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // Modify the request to point to our mock server + req.URL.Scheme = "http" req.URL.Host = m.mockServerURL[len("http://"):] // Remove "http://" + return http.DefaultTransport.RoundTrip(req) } +/* func TestGetAPIPriceData(t *testing.T) { - // Create mock response - fakebody := []GlobalPriceData{ - { - Time_start: fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, - time.Now().Local().Year(), - int(time.Now().Local().Month()), - time.Now().Local().Day(), - time.Now().Local().Hour()), + // Create mock response + fakebody := []GlobalPriceData{ + { + Time_start: fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, + time.Now().Local().Year(), + int(time.Now().Local().Month()), + time.Now().Local().Day(), + time.Now().Local().Hour()), + + SEK_price: 1.23, + }, + } + + fakebody := fmt.Sprintf(priceExample) + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakebody)), + } + mockData, _ := json.Marshal(fakebody) + + // Start a mock HTTP server + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(resp) // this simulated a succesfull response (status 2000) + w.Write(mockData) + })) + defer mockServer.Close() + + // Override the default HTTP client with our mock transport + client := &http.Client{ + Transport: &MockTransport{mockServerURL: mockServer.URL}, + } + + // Temporarily replace the global HTTP client + originalClient := http.DefaultClient + http.DefaultClient = client + defer func() { http.DefaultClient = originalClient }() // Restore after test + + // Call the function (which now hits the mock server) + getAPIPriceData() - SEK_price: 1.23, - }, + // Check if the correct price is stored + expectedPrice := 1.23 + if globalPrice.SEK_price != expectedPrice { + t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) + } } - mockData, _ := json.Marshal(fakebody) +*/ +func TestGetAPIPriceData(t *testing.T) { + // Create fake response body for testing + fakeBody := fmt.Sprintf(priceExample, time.Now().Local().Hour(), time.Now().Local().Hour()+1) // Start a mock HTTP server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) // this simulated a succesfull response (status 2000) - w.Write(mockData) + w.WriteHeader(http.StatusOK) // Simulate a successful response (status 200) + w.Write([]byte(fakeBody)) // Write the fake body to the response })) defer mockServer.Close() From a4c09cd05efeeabacf647b3aa4a61a74c16aea6e Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 12:48:20 +0100 Subject: [PATCH 054/102] not there yet, but the push tests are woring fine --- Comfortstat/api_fetch_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 3c32e70..7b061cc 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -7,7 +7,6 @@ import ( "io" "log" "net/http" - "net/http/httptest" "strings" "testing" "time" @@ -398,6 +397,7 @@ func Test_processfeedbackLoop(t *testing.T) { } */ +/* // Custom RoundTripper to intercept HTTP requests type MockTransport struct { mockServerURL string @@ -412,7 +412,7 @@ func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { return http.DefaultTransport.RoundTrip(req) } - +*/ /* func TestGetAPIPriceData(t *testing.T) { @@ -464,6 +464,7 @@ func TestGetAPIPriceData(t *testing.T) { } } */ +/* func TestGetAPIPriceData(t *testing.T) { // Create fake response body for testing fakeBody := fmt.Sprintf(priceExample, time.Now().Local().Hour(), time.Now().Local().Hour()+1) @@ -494,3 +495,4 @@ func TestGetAPIPriceData(t *testing.T) { t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) } } +*/ From 1e40839839ee1b0d4dfdbef31f87e9e845528121 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 12:50:33 +0100 Subject: [PATCH 055/102] not there yet, but the pushed tests are working fine --- Comfortstat/api_fetch_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 7b061cc..6ca64fb 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -344,6 +344,7 @@ func Test_specialcalculate(t *testing.T) { } } +// TODO: test getApi function /* func Test_processfeedbackLoop(t *testing.T) { ua := initTemplate().(*UnitAsset) From 6b2253afb232a2041104cf2a739745ab024f3b8b Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 15:05:43 +0100 Subject: [PATCH 056/102] more tests --- Comfortstat/api_fetch_test.go | 198 +++++++++++----------------------- Comfortstat/things.go | 41 +++---- 2 files changed, 81 insertions(+), 158 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 6ca64fb..7d0a83f 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -20,11 +20,13 @@ import ( // http.DefaultClient) and it will intercept network requests. type mockTransport struct { + resp *http.Response hits map[string]int } -func newMockTransport() mockTransport { +func newMockTransport(resp *http.Response) mockTransport { t := mockTransport{ + resp: resp, hits: make(map[string]int), } // Highjack the default http client so no actuall http requests are sent over the network @@ -60,15 +62,11 @@ const priceExample string = `[{ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { hour := time.Now().Local().Hour() fakeBody := fmt.Sprintf(priceExample, hour, hour+1) - // TODO: should be able to adjust these return values for the error cases - resp = &http.Response{ - Status: "200 OK", - StatusCode: 200, - Request: req, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } + + t.resp.Body = io.NopCloser(strings.NewReader(fakeBody)) t.hits[req.URL.Hostname()] += 1 - return + t.resp.Request = req + return t.resp, nil } //////////////////////////////////////////////////////////////////////////////// @@ -83,7 +81,12 @@ func TestAPIDataFetchPeriod(t *testing.T) { } func TestSingleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + //Body: io.NopCloser(strings.NewReader(fakeBody)), + } + trans := newMockTransport(resp) // Creates a single UnitAsset and assert it only sends a single API request ua := initTemplate().(*UnitAsset) retrieveAPI_price(ua) @@ -98,7 +101,12 @@ func TestSingleUnitAssetOneAPICall(t *testing.T) { } func TestMultipleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + //Body: io.NopCloser(strings.NewReader(fakeBody)), + } + trans := newMockTransport(resp) // Creates multiple UnitAssets and monitor their API requests units := 10 for i := 0; i < units; i++ { @@ -345,59 +353,7 @@ func Test_specialcalculate(t *testing.T) { } // TODO: test getApi function -/* -func Test_processfeedbackLoop(t *testing.T) { - ua := initTemplate().(*UnitAsset) - - // Set the calculateDesiredTemp function to simulate a new temperature value - ua.calculateDesiredTemp = func() float64 { - return 23.0 // Just return a new temp value to trigger a change - } - - // Override the Pack function to simulate no error and return dummy data - usecases.Pack = func(form *forms.SignalA_v1a, contentType string) ([]byte, error) { - return []byte("packed data"), nil - } - - // Override the SetState function to simulate a successful update - usecases.SetState = func(setpoint interface{}, owner interface{}, op []byte) error { - return nil - } - - // Create a variable to hold the SignalA_v1a form to compare later - // Set the form's value, unit, and timestamp to simulate what the method does - var of forms.SignalA_v1a - of.NewForm() - of.Value = ua.Desired_temp - of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] // This matches the code that fetches the "Unit" - of.Timestamp = time.Now() - - // Run the processFeedbackLoop method - //ua.processFeedbackLoop() - - // Check if the Desired_temp was updated - if ua.Desired_temp != 23.0 { - t.Errorf("Expected Desired_temp to be 23.0, but got %f", ua.Desired_temp) - } - - // Check if the old_desired_temp was updated - if ua.old_desired_temp != 23.0 { - t.Errorf("Expected old_desired_temp to be 23.0, but got %f", ua.old_desired_temp) - } - - - // Optionally, check if the values in the form match what was expected - if of.Value != ua.Desired_temp { - t.Errorf("Expected form Value to be %f, but got %f", ua.Desired_temp, of.Value) - } - - if of.Unit != "Celsius" { - t.Errorf("Expected form Unit to be 'Celsius', but got '%s'", of.Unit) - } - -} -*/ /* // Custom RoundTripper to intercept HTTP requests type MockTransport struct { @@ -414,86 +370,62 @@ func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { return http.DefaultTransport.RoundTrip(req) } */ -/* -func TestGetAPIPriceData(t *testing.T) { +// Fuctions that help creating bad body +type errReader int - // Create mock response - fakebody := []GlobalPriceData{ - { - Time_start: fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, - time.Now().Local().Year(), - int(time.Now().Local().Month()), - time.Now().Local().Day(), - time.Now().Local().Hour()), - - SEK_price: 1.23, - }, - } - - fakebody := fmt.Sprintf(priceExample) - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakebody)), - } - mockData, _ := json.Marshal(fakebody) - - // Start a mock HTTP server - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(resp) // this simulated a succesfull response (status 2000) - w.Write(mockData) - })) - defer mockServer.Close() - - // Override the default HTTP client with our mock transport - client := &http.Client{ - Transport: &MockTransport{mockServerURL: mockServer.URL}, - } +var errBodyRead error = fmt.Errorf("bad body read") - // Temporarily replace the global HTTP client - originalClient := http.DefaultClient - http.DefaultClient = client - defer func() { http.DefaultClient = originalClient }() // Restore after test +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} - // Call the function (which now hits the mock server) - getAPIPriceData() +func (errReader) Close() error { + return nil +} - // Check if the correct price is stored - expectedPrice := 1.23 - if globalPrice.SEK_price != expectedPrice { - t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) - } - } -*/ -/* -func TestGetAPIPriceData(t *testing.T) { - // Create fake response body for testing - fakeBody := fmt.Sprintf(priceExample, time.Now().Local().Hour(), time.Now().Local().Hour()+1) +var brokenURL string = string([]byte{0x7f}) - // Start a mock HTTP server - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) // Simulate a successful response (status 200) - w.Write([]byte(fakeBody)) // Write the fake body to the response - })) - defer mockServer.Close() +func TestGetAPIPriceData(t *testing.T) { - // Override the default HTTP client with our mock transport - client := &http.Client{ - Transport: &MockTransport{mockServerURL: mockServer.URL}, + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader("")), } + url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + newMockTransport(resp) + err := getAPIPriceData(url) // goal is no errors - // Temporarily replace the global HTTP client - originalClient := http.DefaultClient - http.DefaultClient = client - defer func() { http.DefaultClient = originalClient }() // Restore after test + if err != nil { + t.Errorf("expected no errors but got %s :", err) + } + newMockTransport(resp) // Call the function (which now hits the mock server) - getAPIPriceData() + err = getAPIPriceData(brokenURL) + + // Testing bad cases + + // using wrong url leads to an error + if err == nil { + t.Errorf("Expected an error but got none!") - // Check if the correct price is stored - expectedPrice := 1.23 - if globalPrice.SEK_price != expectedPrice { - t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) } + + // Test if reading the body causes an error + resp.Body = errReader(0) + newMockTransport(resp) + err = getAPIPriceData(url) + + if err != errBodyRead { + t.Errorf("expected an error %v, got %v", errBodyRead, err) + } + + /* + // Check if the correct price is stored + expectedPrice := 0.26673 + if globalPrice.SEK_price != expectedPrice { + t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) + } + */ } -*/ diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 1761007..f9d7ff5 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -42,22 +42,13 @@ type UnitAsset struct { // Period time.Duration `json:"samplingPeriod"` // - Daily_prices []API_data `json:"-"` - Desired_temp float64 `json:"desired_temp"` - old_desired_temp float64 // keep this field private! - SEK_price float64 `json:"SEK_per_kWh"` - Min_price float64 `json:"min_price"` - Max_price float64 `json:"max_price"` - Min_temp float64 `json:"min_temp"` - Max_temp float64 `json:"max_temp"` -} - -type API_data struct { - SEK_price float64 `json:"SEK_per_kWh"` - EUR_price float64 `json:"EUR_per_kWh"` - EXR float64 `json:"EXR"` - Time_start string `json:"time_start"` - Time_end string `json:"time_end"` + Desired_temp float64 `json:"desired_temp"` + old_desired_temp float64 // keep this field private! + SEK_price float64 `json:"SEK_per_kWh"` + Min_price float64 `json:"min_price"` + Max_price float64 `json:"max_price"` + Min_temp float64 `json:"min_temp"` + Max_temp float64 `json:"max_temp"` } func initAPI() { @@ -69,9 +60,10 @@ func priceFeedbackLoop() { ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) defer ticker.Stop() + url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) // start the control loop for { - getAPIPriceData() + getAPIPriceData(url) select { case <-ticker.C: // Block the loop until the next period @@ -79,19 +71,17 @@ func priceFeedbackLoop() { } } -func getAPIPriceData() { - url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) - log.Println("URL:", url) +func getAPIPriceData(url string) error { res, err := http.Get(url) if err != nil { - log.Println("Couldn't get the url, error:", err) - return + return err } + body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { log.Println("Something went wrong while reading the body during discovery, error:", err) - return + return err } var data []GlobalPriceData // Create a list to hold the gateway json err = json.Unmarshal(body, &data) // "unpack" body from []byte to []GlobalPriceData, save errors @@ -99,11 +89,11 @@ func getAPIPriceData() { if res.StatusCode > 299 { log.Printf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) - return + return err } if err != nil { log.Println("Error during Unmarshal, error:", err) - return + return err } ///////// @@ -116,6 +106,7 @@ func getAPIPriceData() { } log.Println("current el-pris is:", globalPrice.SEK_price) + return nil } // GetName returns the name of the Resource. From f037c09564b619cae0af20fa183295b643446fa4 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Jan 2025 15:18:57 +0100 Subject: [PATCH 057/102] fixes testing bad body --- Comfortstat/api_fetch_test.go | 32 ++++++++++++++++---------------- Comfortstat/things.go | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 7d0a83f..7675e3d 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -60,10 +60,6 @@ const priceExample string = `[{ // a domain was requested. func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) - - t.resp.Body = io.NopCloser(strings.NewReader(fakeBody)) t.hits[req.URL.Hostname()] += 1 t.resp.Request = req return t.resp, nil @@ -386,37 +382,41 @@ func (errReader) Close() error { var brokenURL string = string([]byte{0x7f}) func TestGetAPIPriceData(t *testing.T) { - + hour := time.Now().Local().Hour() + fakeBody := fmt.Sprintf(priceExample, hour, hour+1) resp := &http.Response{ Status: "200 OK", StatusCode: 200, - Body: io.NopCloser(strings.NewReader("")), + Body: io.NopCloser(strings.NewReader(fakeBody)), } - url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) - newMockTransport(resp) - err := getAPIPriceData(url) // goal is no errors + // Testing good cases + + // Test case: goal is no errors + url := fmt.Sprintf( + `https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, + time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), + ) + newMockTransport(resp) + err := getAPIPriceData(url) if err != nil { t.Errorf("expected no errors but got %s :", err) } + // Testing bad cases + + // Test case: using wrong url leads to an error newMockTransport(resp) // Call the function (which now hits the mock server) err = getAPIPriceData(brokenURL) - - // Testing bad cases - - // using wrong url leads to an error if err == nil { t.Errorf("Expected an error but got none!") - } - // Test if reading the body causes an error + // Test case: if reading the body causes an error resp.Body = errReader(0) newMockTransport(resp) err = getAPIPriceData(url) - if err != errBodyRead { t.Errorf("expected an error %v, got %v", errBodyRead, err) } diff --git a/Comfortstat/things.go b/Comfortstat/things.go index f9d7ff5..fa3957f 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -80,9 +80,9 @@ func getAPIPriceData(url string) error { body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) return err } + var data []GlobalPriceData // Create a list to hold the gateway json err = json.Unmarshal(body, &data) // "unpack" body from []byte to []GlobalPriceData, save errors res.Body.Close() // defer res.Body.Close() From 1176878cfdb8c929bf27e618b2dba76529c7251b Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 18:38:01 +0100 Subject: [PATCH 058/102] test for things.go is completed --- Comfortstat/Comfortstat_test.go | 62 +++++++++++++++++++++++--------- Comfortstat/api_fetch_test.go | 64 ++++++++++++++++++++++++--------- Comfortstat/things.go | 9 ++--- 3 files changed, 96 insertions(+), 39 deletions(-) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index b9e41db..9ff144d 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -1,24 +1,54 @@ package main -/* -func Test_structupdate(t *testing.T) { +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) - asset := UnitAsset{ - Min_temp: 20.0, +func Test_set_SEKprice(t *testing.T) { + ua := initTemplate().(*UnitAsset) + + //Good case test: GET + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/SEK_price", nil) + good_code := 200 + + ua.set_SEKprice(w, r) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + value := strings.Contains(string(body), `"value": 1.5`) + unit := strings.Contains(string(body), `"unit": "SEK"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_code { + t.Errorf("expected good status code: %v, got %v", good_code, resp.StatusCode) + } + + if value != true { + t.Errorf("expected the statment to be true!") + + } + if unit != true { + t.Errorf("expected the unit statement to be true!") } - // Simulate the input signal - inputSignal := forms.SignalA_v1a{ - Value: 17.0, + if version != true { + t.Errorf("expected the version statment to be true!") } - // Call the setMin_temp function - asset.setMin_temp(inputSignal) + // Bad test case: default part of code + w = httptest.NewRecorder() + r = httptest.NewRequest("123", "http://localhost:8670/Comfortstat/Set%20Values/SEK_price", nil) - // Check if Min_temp is updated correctly - if asset.Min_temp != 17.0 { - t.Errorf("expected Min_temp to be 17.0, got %f", asset.Min_temp) + ua.set_SEKprice(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } + } -*/ -// #1 Test if struc can handle floats -// #2 Test if we trys to update the struct, that infact the value is updated correctly -// #3 diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 7675e3d..859974e 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -47,13 +47,14 @@ func (t mockTransport) domainHits(domain string) int { // TODO: this might need to be expanded to a full JSON array? -const priceExample string = `[{ +var priceExample string = fmt.Sprintf(`[{ "SEK_per_kWh": 0.26673, "EUR_per_kWh": 0.02328, "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" - }]` + "time_start": "%d-%02d-%02dT%02d:00:00+01:00", + "time_end": "2025-01-06T04:00:00+01:00" + }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour(), +) // RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). // It prevents the request from being sent over the network and count how many times @@ -238,7 +239,7 @@ func Test_initTemplet(t *testing.T) { name := uasset.GetName() Services := uasset.GetServices() - //Cervices := uasset.GetCervices() + Cervices := uasset.GetCervices() Details := uasset.GetDetails() //// unnecessary test, but good for practicing @@ -268,9 +269,9 @@ func Test_initTemplet(t *testing.T) { t.Errorf("expected service defenition to be desired_temp") } //// Testing GetCervice - //if Cervices == nil { - // t.Fatalf("If cervises is nil, not worth to continue testing") - //} + if Cervices != nil { + t.Fatalf("If cervises not nil, not worth to continue testing") + } //// Testing Details if Details == nil { t.Errorf("expected a map, but Details was nil, ") @@ -382,8 +383,16 @@ func (errReader) Close() error { var brokenURL string = string([]byte{0x7f}) func TestGetAPIPriceData(t *testing.T) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) + priceExample = fmt.Sprintf(`[{ + "SEK_per_kWh": 0.26673, + "EUR_per_kWh": 0.02328, + "EXR": 11.457574, + "time_start": "%d-%02d-%02dT%02d:00:00+01:00", + "time_end": "2025-01-06T04:00:00+01:00" + }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour(), + ) + + fakeBody := fmt.Sprintf(priceExample) resp := &http.Response{ Status: "200 OK", StatusCode: 200, @@ -403,6 +412,13 @@ func TestGetAPIPriceData(t *testing.T) { t.Errorf("expected no errors but got %s :", err) } + // Check if the correct price is stored + expectedPrice := 0.26673 + + if globalPrice.SEK_price != expectedPrice { + t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) + } + // Testing bad cases // Test case: using wrong url leads to an error @@ -421,11 +437,25 @@ func TestGetAPIPriceData(t *testing.T) { t.Errorf("expected an error %v, got %v", errBodyRead, err) } - /* - // Check if the correct price is stored - expectedPrice := 0.26673 - if globalPrice.SEK_price != expectedPrice { - t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) - } - */ + //Test case: if status code > 299 + resp.Body = io.NopCloser(strings.NewReader(fakeBody)) + resp.StatusCode = 300 + newMockTransport(resp) + err = getAPIPriceData(url) + + if err != err_statuscode { + t.Errorf("expected an bad status code but got %v", err) + + } + + // test case: if unmarshal a bad body creates a error + resp.StatusCode = 200 + resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) + newMockTransport(resp) + err = getAPIPriceData(url) + + if err == nil { + t.Errorf("expected an error, got %v :", err) + } + } diff --git a/Comfortstat/things.go b/Comfortstat/things.go index fa3957f..4f4a687 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -71,6 +71,8 @@ func priceFeedbackLoop() { } } +var err_statuscode error = fmt.Errorf("bad status code") + func getAPIPriceData(url string) error { res, err := http.Get(url) @@ -88,11 +90,9 @@ func getAPIPriceData(url string) error { res.Body.Close() // defer res.Body.Close() if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) - return err + return err_statuscode } if err != nil { - log.Println("Error during Unmarshal, error:", err) return err } @@ -101,11 +101,8 @@ func getAPIPriceData(url string) error { for _, i := range data { if i.Time_start == now { globalPrice.SEK_price = i.SEK_price - log.Println("Price in loop is:", i.SEK_price) } - } - log.Println("current el-pris is:", globalPrice.SEK_price) return nil } From 487a67570917d3ae7560d67b1368e9e216db9de0 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 31 Jan 2025 17:45:15 +0100 Subject: [PATCH 059/102] added working test of the GET parts --- Comfortstat/Comfortstat_test.go | 293 ++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index 9ff144d..c9f188e 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -52,3 +52,296 @@ func Test_set_SEKprice(t *testing.T) { } } + +func Test_set_minTemp(t *testing.T) { + + ua := initTemplate().(*UnitAsset) + /* + //Godd test case: PUT + + // creates a fake request body with JSON data + w := httptest.NewRecorder() + body := bytes.NewReader([]byte(`{"value": 20, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", body) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + //good_statuscode := 200 + + ua.set_minTemp(w, r) + + // save the rsponse and read the body + resp := w.Result() + respbody, _ := io.ReadAll(resp.Body) + + log.Printf("Response Body: %s", string(respbody)) + + value := strings.Contains(string(respbody), `"value": 20`) + unit := strings.Contains(string(respbody), `"unit": "Celsius"`) + version := strings.Contains(string(respbody), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + if value != true { + t.Errorf("expected the statment to be true!") + + } + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + if version != true { + t.Errorf("expected the version statment to be true!") + } + */ + + //Good test case: GET + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) + good_statuscode := 200 + ua.set_minTemp(w, r) + + // save the rsponse and read the body + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + value := strings.Contains(string(body), `"value": 20`) + unit := strings.Contains(string(body), `"unit": "Celsius"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + if value != true { + t.Errorf("expected the statment to be true!") + + } + + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + + if version != true { + t.Errorf("expected the version statment to be true!") + } + + // bad test case: default part of code + + // force the case to hit default statement but alter the method + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) + + ua.set_minTemp(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + + } +} + +func Test_set_maxTemp(t *testing.T) { + ua := initTemplate().(*UnitAsset) + //Good test case: GET + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) + good_statuscode := 200 + ua.set_maxTemp(w, r) + + // save the rsponse and read the body + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + value := strings.Contains(string(body), `"value": 25`) + unit := strings.Contains(string(body), `"unit": "Celsius"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + if value != true { + t.Errorf("expected the statment to be true!") + + } + + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + + if version != true { + t.Errorf("expected the version statment to be true!") + } + + // bad test case: default part of code + + // force the case to hit default statement but alter the method + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) + + ua.set_maxTemp(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + + } +} + +func Test_set_minPrice(t *testing.T) { + ua := initTemplate().(*UnitAsset) + //Good test case: GET + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "localhost:8670/Comfortstat/Set%20Values/max_price", nil) + good_statuscode := 200 + ua.set_minPrice(w, r) + + // save the rsponse and read the body + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + value := strings.Contains(string(body), `"value": 1`) //EVENTUELL BUGG, enligt webb-app minPrice = 0 ( kanske dock är för att jag inte startat sregistrar och orchastrator) + unit := strings.Contains(string(body), `"unit": "SEK"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + if value != true { + t.Errorf("expected the statment to be true!") + + } + + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + + if version != true { + t.Errorf("expected the version statment to be true!") + } + + // bad test case: default part of code + + // force the case to hit default statement but alter the method + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/max_price", nil) + + ua.set_minPrice(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + + } +} + +func Test_set_maxPrice(t *testing.T) { + ua := initTemplate().(*UnitAsset) + //Good test case: GET + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) + good_statuscode := 200 + ua.set_maxPrice(w, r) + + // save the rsponse and read the body + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + value := strings.Contains(string(body), `"value": 2`) + unit := strings.Contains(string(body), `"unit": "SEK"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + if value != true { + t.Errorf("expected the statment to be true!") + + } + + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + + if version != true { + t.Errorf("expected the version statment to be true!") + } + + // bad test case: default part of code + + // force the case to hit default statement but alter the method + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) + + ua.set_maxPrice(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + + } +} + +func Test_set_desiredTemp(t *testing.T) { + ua := initTemplate().(*UnitAsset) + //Good test case: GET + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) + good_statuscode := 200 + ua.set_desiredTemp(w, r) + + // save the rsponse and read the body + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + value := strings.Contains(string(body), `"value": 0`) + unit := strings.Contains(string(body), `"unit": "Celsius"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + if value != true { + t.Errorf("expected the statment to be true!") + + } + + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + + if version != true { + t.Errorf("expected the version statment to be true!") + } + + // bad test case: default part of code + + // force the case to hit default statement but alter the method + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) + + ua.set_desiredTemp(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + + } +} From 2503417c39f33699b311116f1e1ab0e316b0349c Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Sun, 2 Feb 2025 13:23:30 +0100 Subject: [PATCH 060/102] cleaned up and added some comments to clarify diffrent parts --- Comfortstat/Comfortstat.go | 11 ++++------- Comfortstat/things.go | 31 +++++++++++-------------------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index 6ecc158..c705eb7 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -67,7 +67,6 @@ func main() { time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end } -// TODO: change the namne, will get one function for each of the four cases // Serving handles the resources services. NOTE: it exepcts those names from the request URL path func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { switch servicePath { @@ -98,7 +97,9 @@ func (rsc *UnitAsset) set_SEKprice(w http.ResponseWriter, r *http.Request) { } } -// TODO: split up this function to two sepreate function that sets on max and min price. +// All these functions below handles HTTP "PUT" or "GET" requests to modefy or retrieve the MAX/MIN temprature/price and desierd temprature +// For the PUT case - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, +// call the set functions in things.go with the value witch updates the value in the struct func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": @@ -130,10 +131,6 @@ func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { } } -// LOOK AT: I guess that we probable only need to if there is a PUT from user? -// LOOK AT: so not the GET! -// For PUT - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, call set_minMaxprice with the value -// wich updates the value in thge struct func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": @@ -182,5 +179,5 @@ func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { default: http.Error(w, "Method is not supported.", http.StatusNotFound) } - // new branch works!!! + } diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 4f4a687..347b4bb 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -23,6 +23,7 @@ type GlobalPriceData struct { Time_end string `json:"time_end"` } +// initiate "globalPrice" with default values var globalPrice = GlobalPriceData{ SEK_price: 0, EUR_price: 0, @@ -73,6 +74,7 @@ func priceFeedbackLoop() { var err_statuscode error = fmt.Errorf("bad status code") +// This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then prosess it and updates globalPrice func getAPIPriceData(url string) error { res, err := http.Get(url) @@ -96,7 +98,7 @@ func getAPIPriceData(url string) error { return err } - ///////// + // extracts the electriciy price depending on the current time and updates globalPrice now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) for _, i := range data { if i.Time_start == now { @@ -133,9 +135,9 @@ var _ components.UnitAsset = (*UnitAsset)(nil) // initTemplate initializes a new UA and prefils it with some default values. // The returned instance is used for generating the configuration file, whenever it's missing. +// (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) func initTemplate() components.UnitAsset { - // First predefine any exposed services - // (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) + setSEK_price := components.Service{ Definition: "SEK_price", SubPath: "SEK_price", @@ -175,7 +177,7 @@ func initTemplate() components.UnitAsset { } return &UnitAsset{ - // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) + //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) Name: "Set Values", Details: map[string][]string{"Location": {"Kitchen"}}, SEK_price: 1.5, // Example electricity price in SEK per kWh @@ -267,15 +269,7 @@ func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { return f } -/* -// setSEK_price updates the current electric price with the new current electric hourly price -func (ua *UnitAsset) setSEK_price(f forms.SignalA_v1a) { - ua.SEK_price = f.Value - //log.Printf("new electric price: %.1f", f.Value) -} -*/ -///////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////// +//Get and set- metods for MIN/MAX price/temp and desierdTemp // getMin_price is used for reading the current value of Min_price func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { @@ -346,17 +340,15 @@ func (ua *UnitAsset) setDesired_temp(f forms.SignalA_v1a) { log.Printf("new desired temperature: %.1f", f.Value) } -//TODO: This fuction is used for checking the electric price ones every x hours and so on -//TODO: Needs to be modified to match our needs, not using processFeedbacklopp -//TODO: So mayby the period is every hour, call the api to receive the current price ( could be every 24 hours) -//TODO: This function is may be better in the COMFORTSTAT MAIN - +// NOTE// // It's _strongly_ encouraged to not send requests to the API for more than once per hour. // Making this period a private constant prevents a user from changing this value // in the config file. const apiFetchPeriod int = 3600 // feedbackLoop is THE control loop (IPR of the system) +// this loop runs a periodic control loop that continuously fetches the api-price data + func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { // Initialize a ticker for periodic execution ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) @@ -405,8 +397,7 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { } } -// - +// this function adjust and sends a new desierd temprature to the zigbee system func (ua *UnitAsset) processFeedbackLoop() { // get the current best temperature From bad24d0d11e9ad552f0a84635b84127cb092c6bd Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Mon, 3 Feb 2025 11:30:44 +0100 Subject: [PATCH 061/102] added test for the PUT part in Comfortstat.go --- Comfortstat/Comfortstat.go | 11 ++ Comfortstat/Comfortstat_test.go | 217 +++++++++++++++++++++++++------- 2 files changed, 182 insertions(+), 46 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index c705eb7..a73070f 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -106,6 +106,9 @@ func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return + } rsc.setMin_temp(sig) case "GET": @@ -121,6 +124,8 @@ func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return } rsc.setMax_temp(sig) case "GET": @@ -137,6 +142,8 @@ func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return } rsc.setMin_price(sig) case "GET": @@ -154,6 +161,8 @@ func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return } rsc.setMax_price(sig) case "GET": @@ -171,6 +180,8 @@ func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return } rsc.setDesired_temp(sig) case "GET": diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index c9f188e..55fe42b 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "io" "net/http" "net/http/httptest" @@ -56,54 +57,50 @@ func Test_set_SEKprice(t *testing.T) { func Test_set_minTemp(t *testing.T) { ua := initTemplate().(*UnitAsset) - /* - //Godd test case: PUT - // creates a fake request body with JSON data - w := httptest.NewRecorder() - body := bytes.NewReader([]byte(`{"value": 20, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", body) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - //good_statuscode := 200 + //Godd test case: PUT - ua.set_minTemp(w, r) + // creates a fake request body with JSON data + w := httptest.NewRecorder() + fakebody := bytes.NewReader([]byte(`{"value": 20, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + good_statuscode := 200 - // save the rsponse and read the body - resp := w.Result() - respbody, _ := io.ReadAll(resp.Body) + ua.set_minTemp(w, r) - log.Printf("Response Body: %s", string(respbody)) + // save the rsponse and read the body + resp := w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } - value := strings.Contains(string(respbody), `"value": 20`) - unit := strings.Contains(string(respbody), `"unit": "Celsius"`) - version := strings.Contains(string(respbody), `"version": "SignalA_v1.0"`) + //BAD case: PUT, if the fake body is formatted incorrectly - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) - } + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - if value != true { - t.Errorf("expected the statment to be true!") + ua.set_minTemp(w, r) - } - if unit != true { - t.Errorf("expected the unit statement to be true!") - } - if version != true { - t.Errorf("expected the version statment to be true!") - } - */ + // save the rsponse and read the body + resp = w.Result() + if resp.StatusCode == good_statuscode { + t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + } //Good test case: GET - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) - good_statuscode := 200 + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) + good_statuscode = 200 ua.set_minTemp(w, r) // save the rsponse and read the body - resp := w.Result() + resp = w.Result() body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 20`) @@ -145,16 +142,48 @@ func Test_set_minTemp(t *testing.T) { func Test_set_maxTemp(t *testing.T) { ua := initTemplate().(*UnitAsset) - //Good test case: GET + //Godd test case: PUT + // creates a fake request body with JSON data w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) + fakebody := bytes.NewReader([]byte(`{"value": 25, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 + ua.set_maxTemp(w, r) // save the rsponse and read the body - resp := w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + //BAD case: PUT, if the fake body is formatted incorrectly + + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + + ua.set_maxTemp(w, r) + + // save the rsponse and read the body + resp = w.Result() + if resp.StatusCode == good_statuscode { + t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + } + //Good test case: GET + + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) + good_statuscode = 200 + ua.set_maxTemp(w, r) + + // save the rsponse and read the body + + resp = w.Result() body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 25`) @@ -196,16 +225,48 @@ func Test_set_maxTemp(t *testing.T) { func Test_set_minPrice(t *testing.T) { ua := initTemplate().(*UnitAsset) - //Good test case: GET + //Godd test case: PUT + // creates a fake request body with JSON data w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "localhost:8670/Comfortstat/Set%20Values/max_price", nil) + fakebody := bytes.NewReader([]byte(`{"value": 1, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/min_price", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 + ua.set_minPrice(w, r) // save the rsponse and read the body - resp := w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + //BAD case: PUT, if the fake body is formatted incorrectly + + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/min_price", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + + ua.set_minPrice(w, r) + + // save the rsponse and read the body + resp = w.Result() + if resp.StatusCode == good_statuscode { + t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + } + //Good test case: GET + + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "localhost:8670/Comfortstat/Set%20Values/min_price", nil) + good_statuscode = 200 + ua.set_minPrice(w, r) + + // save the rsponse and read the body + + resp = w.Result() body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 1`) //EVENTUELL BUGG, enligt webb-app minPrice = 0 ( kanske dock är för att jag inte startat sregistrar och orchastrator) @@ -233,7 +294,7 @@ func Test_set_minPrice(t *testing.T) { // force the case to hit default statement but alter the method w = httptest.NewRecorder() - r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/max_price", nil) + r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/min_price", nil) ua.set_minPrice(w, r) @@ -247,16 +308,48 @@ func Test_set_minPrice(t *testing.T) { func Test_set_maxPrice(t *testing.T) { ua := initTemplate().(*UnitAsset) - //Good test case: GET + //Godd test case: PUT + // creates a fake request body with JSON data w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) + fakebody := bytes.NewReader([]byte(`{"value": 2, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/max_price", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 + ua.set_maxPrice(w, r) // save the rsponse and read the body - resp := w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + //BAD case: PUT, if the fake body is formatted incorrectly + + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/max_price", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + + ua.set_maxPrice(w, r) + + // save the rsponse and read the body + resp = w.Result() + if resp.StatusCode == good_statuscode { + t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + } + //Good test case: GET + + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) + good_statuscode = 200 + ua.set_maxPrice(w, r) + + // save the rsponse and read the body + + resp = w.Result() body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 2`) @@ -298,16 +391,48 @@ func Test_set_maxPrice(t *testing.T) { func Test_set_desiredTemp(t *testing.T) { ua := initTemplate().(*UnitAsset) - //Good test case: GET + //Godd test case: PUT + // creates a fake request body with JSON data w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) + fakebody := bytes.NewReader([]byte(`{"value": 0, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/desired_temp", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 + ua.set_desiredTemp(w, r) // save the rsponse and read the body - resp := w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + + //BAD case: PUT, if the fake body is formatted incorrectly + + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/desired_temp", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + + ua.set_desiredTemp(w, r) + + // save the rsponse and read the body + resp = w.Result() + if resp.StatusCode == good_statuscode { + t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + } + //Good test case: GET + + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) + good_statuscode = 200 + ua.set_desiredTemp(w, r) + + // save the rsponse and read the body + + resp = w.Result() body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 0`) From f7f46735b29e9af9230c341da77b0dc2005a37cf Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Mon, 3 Feb 2025 12:36:12 +0100 Subject: [PATCH 062/102] Updated newUnitAsset --- Comfortstat/Comfortstat.go | 4 +- Comfortstat/api_fetch_test.go | 112 ++++++++++++++++++++-------------- Comfortstat/things.go | 14 ++--- 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index a73070f..1e88770 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -46,8 +46,8 @@ func main() { if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newUnitAsset(uac, &sys, servsTemp) - defer cleanup() + ua, startup := newUnitAsset(uac, &sys, servsTemp) + startup() sys.UAssets[ua.GetName()] = &ua } diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 859974e..7fc165c 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -2,10 +2,8 @@ package main import ( "context" - "encoding/json" "fmt" "io" - "log" "net/http" "strings" "testing" @@ -13,7 +11,6 @@ import ( "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" ) // mockTransport is used for replacing the default network Transport (used by @@ -295,33 +292,74 @@ func Test_newUnitAsset(t *testing.T) { ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", } - - // instantiate a template unit asset - assetTemplate := initTemplate() - //initAPI() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate - - // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, cleanup := newUnitAsset(uac, &sys, servsTemp) - defer cleanup() - sys.UAssets[ua.GetName()] = &ua + setSEK_price := components.Service{ + Definition: "SEK_price", + SubPath: "SEK_price", + Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current electric hourly price (using a GET request)", + } + + setMax_temp := components.Service{ + Definition: "max_temperature", + SubPath: "max_temperature", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the maximum temp the user wants (using a GET request)", + } + setMin_temp := components.Service{ + Definition: "min_temperature", + SubPath: "min_temperature", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the minimum temp the user could tolerate (using a GET request)", + } + setMax_price := components.Service{ + Definition: "max_price", + SubPath: "max_price", + Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the maximum price the user wants to pay (using a GET request)", + } + setMin_price := components.Service{ + Definition: "min_price", + SubPath: "min_price", + Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the minimum price the user wants to pay (using a GET request)", + } + setDesired_temp := components.Service{ + Definition: "desired_temp", + SubPath: "desired_temp", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", + } + + uac := UnitAsset{ + //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) + Name: "Set Values", + Details: map[string][]string{"Location": {"Kitchen"}}, + SEK_price: 1.5, // Example electricity price in SEK per kWh + Min_price: 1.0, // Minimum price allowed + Max_price: 2.0, // Maximum price allowed + Min_temp: 20.0, // Minimum temperature + Max_temp: 25.0, // Maximum temprature allowed + Desired_temp: 0, // Desired temp calculated by system + Period: 15, + + // maps the provided services from above + ServicesMap: components.Services{ + setMax_temp.SubPath: &setMax_temp, + setMin_temp.SubPath: &setMin_temp, + setMax_price.SubPath: &setMax_price, + setMin_price.SubPath: &setMin_price, + setSEK_price.SubPath: &setSEK_price, + setDesired_temp.SubPath: &setDesired_temp, + }, + } + + ua, _ := newUnitAsset(uac, &sys, nil) + + name := ua.GetName() + if name != "Set Values" { + t.Errorf("expected name to be Set values, but got: %v", name) } - // Skriv if-satser som kollar namn och services - // testa calculatedeiserdTemp(nytt test) - // processfeedbackloop(nytt test) - // } func Test_calculateDesiredTemp(t *testing.T) { @@ -349,24 +387,6 @@ func Test_specialcalculate(t *testing.T) { } } -// TODO: test getApi function - -/* -// Custom RoundTripper to intercept HTTP requests -type MockTransport struct { - mockServerURL string -} - -// Implement the RoundTrip function for MockTransport, here is where the logic on how HTTP request are handled -// modify the request to point at the created mock server -func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - - req.URL.Scheme = "http" - req.URL.Host = m.mockServerURL[len("http://"):] // Remove "http://" - - return http.DefaultTransport.RoundTrip(req) -} -*/ // Fuctions that help creating bad body type errReader int diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 347b4bb..befbb3f 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -245,18 +245,12 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv ua.CervicesMap["setpoint"].Details = components.MergeDetails(ua.Details, ref.Details) - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - go ua.API_feedbackLoop(sys.Ctx) - - // Optionally start background tasks here! Example: - go func() { - log.Println("Starting up " + ua.Name) - }() - // Returns the loaded unit asset and an function to handle optional cleanup at shutdown return ua, func() { - log.Println("Cleaning up " + ua.Name) + // start the unit asset(s) + go ua.feedbackLoop(sys.Ctx) + go ua.API_feedbackLoop(sys.Ctx) + } } From f86d7392efbe2dcbc6e41f46bc295b551d5d9454 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Mon, 3 Feb 2025 12:58:51 +0100 Subject: [PATCH 063/102] updated getapiprice function to match linter tests --- Comfortstat/things.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index befbb3f..838d3e3 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -75,9 +75,9 @@ func priceFeedbackLoop() { var err_statuscode error = fmt.Errorf("bad status code") // This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then prosess it and updates globalPrice -func getAPIPriceData(url string) error { +func getAPIPriceData(apiURL string) error { - res, err := http.Get(url) + res, err := http.Get(apiURL) if err != nil { return err } @@ -245,7 +245,7 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv ua.CervicesMap["setpoint"].Details = components.MergeDetails(ua.Details, ref.Details) - // Returns the loaded unit asset and an function to handle optional cleanup at shutdown + // Returns the loaded unit asset and an function to handle return ua, func() { // start the unit asset(s) go ua.feedbackLoop(sys.Ctx) From c095e91857e71d28555f0ce3381aa71d2a77ab05 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 09:43:14 +0100 Subject: [PATCH 064/102] added a part that validate the url in getApiPricedata --- Comfortstat/things.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 838d3e3..0b1a080 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -3,11 +3,13 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "io" "log" "math" "net/http" + "net/url" "time" "github.com/sdoque/mbaigo/components" @@ -76,8 +78,13 @@ var err_statuscode error = fmt.Errorf("bad status code") // This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then prosess it and updates globalPrice func getAPIPriceData(apiURL string) error { - - res, err := http.Get(apiURL) + //Validate the URL// + parsedURL, err := url.Parse(apiURL) // ensures the string is a valid UTL, .schema and .Host checks prevent emty or altered URL + if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { + return errors.New("invalid URL") + } + // end of validating the URL// + res, err := http.Get(parsedURL.String()) if err != nil { return err } From 962f02efdfdd0a1a645f2808a87afc577a154c38 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 09:57:42 +0100 Subject: [PATCH 065/102] added error handling in getApiPricedata --- Comfortstat/things.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 0b1a080..c65a0e9 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -79,9 +79,9 @@ var err_statuscode error = fmt.Errorf("bad status code") // This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then prosess it and updates globalPrice func getAPIPriceData(apiURL string) error { //Validate the URL// - parsedURL, err := url.Parse(apiURL) // ensures the string is a valid UTL, .schema and .Host checks prevent emty or altered URL + parsedURL, err := url.Parse(apiURL) // ensures the string is a valid URL, .schema and .Host checks prevent emty or altered URL if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { - return errors.New("invalid URL") + return errors.New("The URL is invalid") } // end of validating the URL// res, err := http.Get(parsedURL.String()) @@ -94,9 +94,14 @@ func getAPIPriceData(apiURL string) error { return err } - var data []GlobalPriceData // Create a list to hold the gateway json + var data []GlobalPriceData // Create a list to hold the data json err = json.Unmarshal(body, &data) // "unpack" body from []byte to []GlobalPriceData, save errors - res.Body.Close() // defer res.Body.Close() + /* + if err != nil { + return err + } + */ + defer res.Body.Close() // defer res.Body.Close() if res.StatusCode > 299 { return err_statuscode From 41cf05a35f5e3836eef22ea63680daa052157bd5 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:08:21 +0100 Subject: [PATCH 066/102] added error handling in the pricefeedbackloop --- Comfortstat/things.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index c65a0e9..553cd25 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -66,10 +66,14 @@ func priceFeedbackLoop() { url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) // start the control loop for { - getAPIPriceData(url) + err := getAPIPriceData(url) + + if err != nil { + return + } select { + case <-ticker.C: - // Block the loop until the next period } } } @@ -96,12 +100,8 @@ func getAPIPriceData(apiURL string) error { var data []GlobalPriceData // Create a list to hold the data json err = json.Unmarshal(body, &data) // "unpack" body from []byte to []GlobalPriceData, save errors - /* - if err != nil { - return err - } - */ - defer res.Body.Close() // defer res.Body.Close() + + defer res.Body.Close() if res.StatusCode > 299 { return err_statuscode From d903944a99e8cf596200abf1b687c9eafae10c63 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:13:30 +0100 Subject: [PATCH 067/102] added error handling in pricefeedbackloop --- Comfortstat/things.go | 1 + 1 file changed, 1 insertion(+) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 553cd25..2195f6f 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -74,6 +74,7 @@ func priceFeedbackLoop() { select { case <-ticker.C: + // blocks the execution until the ticker fires } } } From fcf56c304197f6d1aa6405d4eedc849265264eff Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:21:56 +0100 Subject: [PATCH 068/102] cleaned up some log.prints in the set-functions --- Comfortstat/Comfortstat.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index 1e88770..3663aa3 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -105,7 +105,7 @@ func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) http.Error(w, "request incorreclty formated", http.StatusBadRequest) return @@ -123,7 +123,7 @@ func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } @@ -141,7 +141,7 @@ func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } @@ -160,7 +160,7 @@ func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } @@ -179,7 +179,7 @@ func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } From 6d04361bbda2674f5a3cae0bfdd5a3812ffda623 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:59:56 +0100 Subject: [PATCH 069/102] moved the check for the statuscode to the right place, before reading the body part and cleand up in api_fetch_test.go file --- Comfortstat/Comfortstat_test.go | 43 +++++++++++++++------------------ Comfortstat/api_fetch_test.go | 22 +++++------------ 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index 55fe42b..ebf4960 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -20,16 +20,16 @@ func Test_set_SEKprice(t *testing.T) { ua.set_SEKprice(w, r) resp := w.Result() + if resp.StatusCode != good_code { + t.Errorf("expected good status code: %v, got %v", good_code, resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 1.5`) unit := strings.Contains(string(body), `"unit": "SEK"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if resp.StatusCode != good_code { - t.Errorf("expected good status code: %v, got %v", good_code, resp.StatusCode) - } - if value != true { t.Errorf("expected the statment to be true!") @@ -101,16 +101,15 @@ func Test_set_minTemp(t *testing.T) { // save the rsponse and read the body resp = w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 20`) unit := strings.Contains(string(body), `"unit": "Celsius"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) - } - if value != true { t.Errorf("expected the statment to be true!") @@ -184,16 +183,16 @@ func Test_set_maxTemp(t *testing.T) { // save the rsponse and read the body resp = w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 25`) unit := strings.Contains(string(body), `"unit": "Celsius"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) - } - if value != true { t.Errorf("expected the statment to be true!") @@ -267,16 +266,15 @@ func Test_set_minPrice(t *testing.T) { // save the rsponse and read the body resp = w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 1`) //EVENTUELL BUGG, enligt webb-app minPrice = 0 ( kanske dock är för att jag inte startat sregistrar och orchastrator) unit := strings.Contains(string(body), `"unit": "SEK"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) - } - if value != true { t.Errorf("expected the statment to be true!") @@ -350,16 +348,15 @@ func Test_set_maxPrice(t *testing.T) { // save the rsponse and read the body resp = w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 2`) unit := strings.Contains(string(body), `"unit": "SEK"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) - } - if value != true { t.Errorf("expected the statment to be true!") @@ -433,15 +430,15 @@ func Test_set_desiredTemp(t *testing.T) { // save the rsponse and read the body resp = w.Result() + if resp.StatusCode != good_statuscode { + t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + } body, _ := io.ReadAll(resp.Body) value := strings.Contains(string(body), `"value": 0`) unit := strings.Contains(string(body), `"unit": "Celsius"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) - } if value != true { t.Errorf("expected the statment to be true!") diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index 7fc165c..fa7ad12 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -42,8 +42,7 @@ func (t mockTransport) domainHits(domain string) int { return -1 } -// TODO: this might need to be expanded to a full JSON array? - +// price example string in a JSON-like format var priceExample string = fmt.Sprintf(`[{ "SEK_per_kWh": 0.26673, "EUR_per_kWh": 0.02328, @@ -90,15 +89,12 @@ func TestSingleUnitAssetOneAPICall(t *testing.T) { if hits > 1 { t.Errorf("expected number of api requests = 1, got %d requests", hits) } - - // TODO: try more test cases! } func TestMultipleUnitAssetOneAPICall(t *testing.T) { resp := &http.Response{ Status: "200 OK", StatusCode: 200, - //Body: io.NopCloser(strings.NewReader(fakeBody)), } trans := newMockTransport(resp) // Creates multiple UnitAssets and monitor their API requests @@ -113,8 +109,6 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { if hits > 1 { t.Errorf("expected number of api requests = 1, got %d requests (from %d units)", hits, units) } - - // TODO: more test cases?? } func TestSetmethods(t *testing.T) { @@ -183,8 +177,9 @@ func Test_GetMethods(t *testing.T) { //check that the Unit is correct if result.Unit != "Celsius" { t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) - ////MaxTemp//// + } + ////MaxTemp//// if result2.Value != uasset.Max_temp { t.Errorf("expected Value of the Max_temp is to be %v, got %v", uasset.Max_temp, result2.Value) } @@ -224,11 +219,6 @@ func Test_GetMethods(t *testing.T) { if result6.Value != uasset.SEK_price { t.Errorf("expected electric price is to be %v, got %v", uasset.SEK_price, result6.Value) } - //check that the Unit is correct - //if result5.Unit != "SEK" { - // t.Errorf("expected Unit to be 'SEK', got %v", result6.Unit) - //} - } func Test_initTemplet(t *testing.T) { @@ -246,7 +236,7 @@ func Test_initTemplet(t *testing.T) { if Services == nil { t.Fatalf("If Services is nil, not worth to continue testing") } - ////Services//// + //Services// if Services["SEK_price"].Definition != "SEK_price" { t.Errorf("expected service defenition to be SEKprice") } @@ -265,11 +255,11 @@ func Test_initTemplet(t *testing.T) { if Services["desired_temp"].Definition != "desired_temp" { t.Errorf("expected service defenition to be desired_temp") } - //// Testing GetCervice + //GetCervice// if Cervices != nil { t.Fatalf("If cervises not nil, not worth to continue testing") } - //// Testing Details + //Testing Details// if Details == nil { t.Errorf("expected a map, but Details was nil, ") } From ac174864645f637a55ae20c5b86e624b6470b17d Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 11:14:49 +0100 Subject: [PATCH 070/102] fixed so that i run the tests directly after the fuction call --- Comfortstat/api_fetch_test.go | 45 ++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go index fa7ad12..b91fc84 100644 --- a/Comfortstat/api_fetch_test.go +++ b/Comfortstat/api_fetch_test.go @@ -132,26 +132,28 @@ func TestSetmethods(t *testing.T) { Value: 23.7, } - // Call the setMin_temp function + //call and test min_temp asset.setMin_temp(MinTemp_inputSignal) - asset.setMax_temp(MaxTemp_inputSignal) - asset.setMin_price(MinPrice_inputSignal) - asset.setMax_price(MaxPrice_inputSignal) - asset.setDesired_temp(DesTemp_inputSignal) - - // check if the temprature has changed correctly if asset.Min_temp != 1.0 { t.Errorf("expected Min_temp to be 1.0, got %f", asset.Min_temp) } + // call and test max_temp + asset.setMax_temp(MaxTemp_inputSignal) if asset.Max_temp != 29.0 { t.Errorf("expected Max_temp to be 25.0, got %f", asset.Max_temp) } + //call and test Min_price + asset.setMin_price(MinPrice_inputSignal) if asset.Min_price != 2.0 { t.Errorf("expected Min_Price to be 2.0, got %f", asset.Min_price) } + //call and test Max_price + asset.setMax_price(MaxPrice_inputSignal) if asset.Max_price != 12.0 { t.Errorf("expected Max_Price to be 12.0, got %f", asset.Max_price) } + // call and test Desired_temp + asset.setDesired_temp(DesTemp_inputSignal) if asset.Desired_temp != 23.7 { t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.Desired_temp) } @@ -161,16 +163,10 @@ func TestSetmethods(t *testing.T) { func Test_GetMethods(t *testing.T) { uasset := initTemplate().(*UnitAsset) - //call the fuctions - result := uasset.getMin_temp() - result2 := uasset.getMax_temp() - result3 := uasset.getMin_price() - result4 := uasset.getMax_price() - result5 := uasset.getDesired_temp() - result6 := uasset.getSEK_price() ////MinTemp//// // check if the value from the struct is the acctual value that the func is getting + result := uasset.getMin_temp() if result.Value != uasset.Min_temp { t.Errorf("expected Value of the min_temp is to be %v, got %v", uasset.Min_temp, result.Value) } @@ -180,6 +176,7 @@ func Test_GetMethods(t *testing.T) { } ////MaxTemp//// + result2 := uasset.getMax_temp() if result2.Value != uasset.Max_temp { t.Errorf("expected Value of the Max_temp is to be %v, got %v", uasset.Max_temp, result2.Value) } @@ -189,6 +186,7 @@ func Test_GetMethods(t *testing.T) { } ////MinPrice//// // check if the value from the struct is the acctual value that the func is getting + result3 := uasset.getMin_price() if result3.Value != uasset.Min_price { t.Errorf("expected Value of the minPrice is to be %v, got %v", uasset.Min_price, result3.Value) } @@ -199,6 +197,7 @@ func Test_GetMethods(t *testing.T) { ////MaxPrice//// // check if the value from the struct is the acctual value that the func is getting + result4 := uasset.getMax_price() if result4.Value != uasset.Max_price { t.Errorf("expected Value of the maxPrice is to be %v, got %v", uasset.Max_price, result4.Value) } @@ -208,6 +207,7 @@ func Test_GetMethods(t *testing.T) { } ////DesierdTemp//// // check if the value from the struct is the acctual value that the func is getting + result5 := uasset.getDesired_temp() if result5.Value != uasset.Desired_temp { t.Errorf("expected desired temprature is to be %v, got %v", uasset.Desired_temp, result5.Value) } @@ -216,6 +216,7 @@ func Test_GetMethods(t *testing.T) { t.Errorf("expected Unit to be 'Celsius', got %v", result5.Unit) } ////SEK_Price//// + result6 := uasset.getSEK_price() if result6.Value != uasset.SEK_price { t.Errorf("expected electric price is to be %v, got %v", uasset.SEK_price, result6.Value) } @@ -223,16 +224,20 @@ func Test_GetMethods(t *testing.T) { func Test_initTemplet(t *testing.T) { uasset := initTemplate().(*UnitAsset) - - name := uasset.GetName() - Services := uasset.GetServices() - Cervices := uasset.GetCervices() - Details := uasset.GetDetails() + /* + name := uasset.GetName() + Services := uasset.GetServices() + Cervices := uasset.GetCervices() + Details := uasset.GetDetails() + */ //// unnecessary test, but good for practicing + + name := uasset.GetName() if name != "Set Values" { t.Errorf("expected name of the resource is %v, got %v", uasset.Name, name) } + Services := uasset.GetServices() if Services == nil { t.Fatalf("If Services is nil, not worth to continue testing") } @@ -256,10 +261,12 @@ func Test_initTemplet(t *testing.T) { t.Errorf("expected service defenition to be desired_temp") } //GetCervice// + Cervices := uasset.GetCervices() if Cervices != nil { t.Fatalf("If cervises not nil, not worth to continue testing") } //Testing Details// + Details := uasset.GetDetails() if Details == nil { t.Errorf("expected a map, but Details was nil, ") } From 960277088c1317e75a1ea408bb95700e66455283 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 11:31:34 +0100 Subject: [PATCH 071/102] changed the name to a more suitable name --- Comfortstat/things_test.go | 478 +++++++++++++++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 Comfortstat/things_test.go diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go new file mode 100644 index 0000000..b91fc84 --- /dev/null +++ b/Comfortstat/things_test.go @@ -0,0 +1,478 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" +) + +// mockTransport is used for replacing the default network Transport (used by +// http.DefaultClient) and it will intercept network requests. + +type mockTransport struct { + resp *http.Response + hits map[string]int +} + +func newMockTransport(resp *http.Response) mockTransport { + t := mockTransport{ + resp: resp, + hits: make(map[string]int), + } + // Highjack the default http client so no actuall http requests are sent over the network + http.DefaultClient.Transport = t + return t +} + +// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). + +func (t mockTransport) domainHits(domain string) int { + for u, hits := range t.hits { + if u == domain { + return hits + } + } + return -1 +} + +// price example string in a JSON-like format +var priceExample string = fmt.Sprintf(`[{ + "SEK_per_kWh": 0.26673, + "EUR_per_kWh": 0.02328, + "EXR": 11.457574, + "time_start": "%d-%02d-%02dT%02d:00:00+01:00", + "time_end": "2025-01-06T04:00:00+01:00" + }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour(), +) + +// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). +// It prevents the request from being sent over the network and count how many times +// a domain was requested. + +func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + t.hits[req.URL.Hostname()] += 1 + t.resp.Request = req + return t.resp, nil +} + +//////////////////////////////////////////////////////////////////////////////// + +const apiDomain string = "www.elprisetjustnu.se" + +func TestAPIDataFetchPeriod(t *testing.T) { + want := 3600 + if apiFetchPeriod < want { + t.Errorf("expected API fetch period >= %d, got %d", want, apiFetchPeriod) + } +} + +func TestSingleUnitAssetOneAPICall(t *testing.T) { + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + //Body: io.NopCloser(strings.NewReader(fakeBody)), + } + trans := newMockTransport(resp) + // Creates a single UnitAsset and assert it only sends a single API request + ua := initTemplate().(*UnitAsset) + retrieveAPI_price(ua) + + // TEST CASE: cause a single API request + hits := trans.domainHits(apiDomain) + if hits > 1 { + t.Errorf("expected number of api requests = 1, got %d requests", hits) + } +} + +func TestMultipleUnitAssetOneAPICall(t *testing.T) { + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + } + trans := newMockTransport(resp) + // Creates multiple UnitAssets and monitor their API requests + units := 10 + for i := 0; i < units; i++ { + ua := initTemplate().(*UnitAsset) + retrieveAPI_price(ua) + } + + // TEST CASE: causing only one API hit while using multiple UnitAssets + hits := trans.domainHits(apiDomain) + if hits > 1 { + t.Errorf("expected number of api requests = 1, got %d requests (from %d units)", hits, units) + } +} + +func TestSetmethods(t *testing.T) { + + asset := initTemplate().(*UnitAsset) + + // Simulate the input signals + MinTemp_inputSignal := forms.SignalA_v1a{ + Value: 1.0, + } + MaxTemp_inputSignal := forms.SignalA_v1a{ + Value: 29.0, + } + MinPrice_inputSignal := forms.SignalA_v1a{ + Value: 2.0, + } + MaxPrice_inputSignal := forms.SignalA_v1a{ + Value: 12.0, + } + DesTemp_inputSignal := forms.SignalA_v1a{ + Value: 23.7, + } + + //call and test min_temp + asset.setMin_temp(MinTemp_inputSignal) + if asset.Min_temp != 1.0 { + t.Errorf("expected Min_temp to be 1.0, got %f", asset.Min_temp) + } + // call and test max_temp + asset.setMax_temp(MaxTemp_inputSignal) + if asset.Max_temp != 29.0 { + t.Errorf("expected Max_temp to be 25.0, got %f", asset.Max_temp) + } + //call and test Min_price + asset.setMin_price(MinPrice_inputSignal) + if asset.Min_price != 2.0 { + t.Errorf("expected Min_Price to be 2.0, got %f", asset.Min_price) + } + //call and test Max_price + asset.setMax_price(MaxPrice_inputSignal) + if asset.Max_price != 12.0 { + t.Errorf("expected Max_Price to be 12.0, got %f", asset.Max_price) + } + // call and test Desired_temp + asset.setDesired_temp(DesTemp_inputSignal) + if asset.Desired_temp != 23.7 { + t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.Desired_temp) + } + +} + +func Test_GetMethods(t *testing.T) { + + uasset := initTemplate().(*UnitAsset) + + ////MinTemp//// + // check if the value from the struct is the acctual value that the func is getting + result := uasset.getMin_temp() + if result.Value != uasset.Min_temp { + t.Errorf("expected Value of the min_temp is to be %v, got %v", uasset.Min_temp, result.Value) + } + //check that the Unit is correct + if result.Unit != "Celsius" { + t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) + + } + ////MaxTemp//// + result2 := uasset.getMax_temp() + if result2.Value != uasset.Max_temp { + t.Errorf("expected Value of the Max_temp is to be %v, got %v", uasset.Max_temp, result2.Value) + } + //check that the Unit is correct + if result2.Unit != "Celsius" { + t.Errorf("expected Unit of the Max_temp is to be 'Celsius', got %v", result2.Unit) + } + ////MinPrice//// + // check if the value from the struct is the acctual value that the func is getting + result3 := uasset.getMin_price() + if result3.Value != uasset.Min_price { + t.Errorf("expected Value of the minPrice is to be %v, got %v", uasset.Min_price, result3.Value) + } + //check that the Unit is correct + if result3.Unit != "SEK" { + t.Errorf("expected Unit to be 'SEK', got %v", result3.Unit) + } + + ////MaxPrice//// + // check if the value from the struct is the acctual value that the func is getting + result4 := uasset.getMax_price() + if result4.Value != uasset.Max_price { + t.Errorf("expected Value of the maxPrice is to be %v, got %v", uasset.Max_price, result4.Value) + } + //check that the Unit is correct + if result4.Unit != "SEK" { + t.Errorf("expected Unit to be 'SEK', got %v", result4.Unit) + } + ////DesierdTemp//// + // check if the value from the struct is the acctual value that the func is getting + result5 := uasset.getDesired_temp() + if result5.Value != uasset.Desired_temp { + t.Errorf("expected desired temprature is to be %v, got %v", uasset.Desired_temp, result5.Value) + } + //check that the Unit is correct + if result5.Unit != "Celsius" { + t.Errorf("expected Unit to be 'Celsius', got %v", result5.Unit) + } + ////SEK_Price//// + result6 := uasset.getSEK_price() + if result6.Value != uasset.SEK_price { + t.Errorf("expected electric price is to be %v, got %v", uasset.SEK_price, result6.Value) + } +} + +func Test_initTemplet(t *testing.T) { + uasset := initTemplate().(*UnitAsset) + /* + name := uasset.GetName() + Services := uasset.GetServices() + Cervices := uasset.GetCervices() + Details := uasset.GetDetails() + */ + + //// unnecessary test, but good for practicing + + name := uasset.GetName() + if name != "Set Values" { + t.Errorf("expected name of the resource is %v, got %v", uasset.Name, name) + } + Services := uasset.GetServices() + if Services == nil { + t.Fatalf("If Services is nil, not worth to continue testing") + } + //Services// + if Services["SEK_price"].Definition != "SEK_price" { + t.Errorf("expected service defenition to be SEKprice") + } + if Services["max_temperature"].Definition != "max_temperature" { + t.Errorf("expected service defenition to be max_temperature") + } + if Services["min_temperature"].Definition != "min_temperature" { + t.Errorf("expected service defenition to be min_temperature") + } + if Services["max_price"].Definition != "max_price" { + t.Errorf("expected service defenition to be max_price") + } + if Services["min_price"].Definition != "min_price" { + t.Errorf("expected service defenition to be min_price") + } + if Services["desired_temp"].Definition != "desired_temp" { + t.Errorf("expected service defenition to be desired_temp") + } + //GetCervice// + Cervices := uasset.GetCervices() + if Cervices != nil { + t.Fatalf("If cervises not nil, not worth to continue testing") + } + //Testing Details// + Details := uasset.GetDetails() + if Details == nil { + t.Errorf("expected a map, but Details was nil, ") + } + +} + +func Test_newUnitAsset(t *testing.T) { + // prepare for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled + defer cancel() // make sure all paths cancel the context to avoid context leak + + // instantiate the System + sys := components.NewSystem("Comfortstat", ctx) + + // Instatiate the Capusle + sys.Husk = &components.Husk{ + Description: " is a controller for a consumed servo motor position based on a consumed temperature", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, + InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", + } + setSEK_price := components.Service{ + Definition: "SEK_price", + SubPath: "SEK_price", + Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current electric hourly price (using a GET request)", + } + + setMax_temp := components.Service{ + Definition: "max_temperature", + SubPath: "max_temperature", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the maximum temp the user wants (using a GET request)", + } + setMin_temp := components.Service{ + Definition: "min_temperature", + SubPath: "min_temperature", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the minimum temp the user could tolerate (using a GET request)", + } + setMax_price := components.Service{ + Definition: "max_price", + SubPath: "max_price", + Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the maximum price the user wants to pay (using a GET request)", + } + setMin_price := components.Service{ + Definition: "min_price", + SubPath: "min_price", + Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the minimum price the user wants to pay (using a GET request)", + } + setDesired_temp := components.Service{ + Definition: "desired_temp", + SubPath: "desired_temp", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", + } + + uac := UnitAsset{ + //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) + Name: "Set Values", + Details: map[string][]string{"Location": {"Kitchen"}}, + SEK_price: 1.5, // Example electricity price in SEK per kWh + Min_price: 1.0, // Minimum price allowed + Max_price: 2.0, // Maximum price allowed + Min_temp: 20.0, // Minimum temperature + Max_temp: 25.0, // Maximum temprature allowed + Desired_temp: 0, // Desired temp calculated by system + Period: 15, + + // maps the provided services from above + ServicesMap: components.Services{ + setMax_temp.SubPath: &setMax_temp, + setMin_temp.SubPath: &setMin_temp, + setMax_price.SubPath: &setMax_price, + setMin_price.SubPath: &setMin_price, + setSEK_price.SubPath: &setSEK_price, + setDesired_temp.SubPath: &setDesired_temp, + }, + } + + ua, _ := newUnitAsset(uac, &sys, nil) + + name := ua.GetName() + if name != "Set Values" { + t.Errorf("expected name to be Set values, but got: %v", name) + } + +} + +func Test_calculateDesiredTemp(t *testing.T) { + var True_result float64 = 22.5 + asset := initTemplate().(*UnitAsset) + + result := asset.calculateDesiredTemp() + + if result != True_result { + t.Errorf("Expected calculated temp is %v, got %v", True_result, result) + } +} + +func Test_specialcalculate(t *testing.T) { + asset := UnitAsset{ + SEK_price: 3.0, + Max_price: 2.0, + Min_temp: 17.0, + } + + result := asset.calculateDesiredTemp() + + if result != asset.Min_temp { + t.Errorf("Expected temperature to be %v, got %v", asset.Min_temp, result) + } +} + +// Fuctions that help creating bad body +type errReader int + +var errBodyRead error = fmt.Errorf("bad body read") + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} + +func (errReader) Close() error { + return nil +} + +var brokenURL string = string([]byte{0x7f}) + +func TestGetAPIPriceData(t *testing.T) { + priceExample = fmt.Sprintf(`[{ + "SEK_per_kWh": 0.26673, + "EUR_per_kWh": 0.02328, + "EXR": 11.457574, + "time_start": "%d-%02d-%02dT%02d:00:00+01:00", + "time_end": "2025-01-06T04:00:00+01:00" + }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour(), + ) + + fakeBody := fmt.Sprintf(priceExample) + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + // Testing good cases + + // Test case: goal is no errors + url := fmt.Sprintf( + `https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, + time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), + ) + newMockTransport(resp) + err := getAPIPriceData(url) + if err != nil { + t.Errorf("expected no errors but got %s :", err) + } + + // Check if the correct price is stored + expectedPrice := 0.26673 + + if globalPrice.SEK_price != expectedPrice { + t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) + } + + // Testing bad cases + + // Test case: using wrong url leads to an error + newMockTransport(resp) + // Call the function (which now hits the mock server) + err = getAPIPriceData(brokenURL) + if err == nil { + t.Errorf("Expected an error but got none!") + } + + // Test case: if reading the body causes an error + resp.Body = errReader(0) + newMockTransport(resp) + err = getAPIPriceData(url) + if err != errBodyRead { + t.Errorf("expected an error %v, got %v", errBodyRead, err) + } + + //Test case: if status code > 299 + resp.Body = io.NopCloser(strings.NewReader(fakeBody)) + resp.StatusCode = 300 + newMockTransport(resp) + err = getAPIPriceData(url) + + if err != err_statuscode { + t.Errorf("expected an bad status code but got %v", err) + + } + + // test case: if unmarshal a bad body creates a error + resp.StatusCode = 200 + resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) + newMockTransport(resp) + err = getAPIPriceData(url) + + if err == nil { + t.Errorf("expected an error, got %v :", err) + } + +} From aabd58e542bedeb0d02c0f032c3a1191ca6f07e9 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Feb 2025 11:49:54 +0100 Subject: [PATCH 072/102] Reverts " changed the name to a more suitable name" This backs out commit 4e22141226bcf6473a716d01eed2439fda8ee295. --- Comfortstat/things_test.go | 478 ------------------------------------- 1 file changed, 478 deletions(-) delete mode 100644 Comfortstat/things_test.go diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go deleted file mode 100644 index b91fc84..0000000 --- a/Comfortstat/things_test.go +++ /dev/null @@ -1,478 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "net/http" - "strings" - "testing" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/forms" -) - -// mockTransport is used for replacing the default network Transport (used by -// http.DefaultClient) and it will intercept network requests. - -type mockTransport struct { - resp *http.Response - hits map[string]int -} - -func newMockTransport(resp *http.Response) mockTransport { - t := mockTransport{ - resp: resp, - hits: make(map[string]int), - } - // Highjack the default http client so no actuall http requests are sent over the network - http.DefaultClient.Transport = t - return t -} - -// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). - -func (t mockTransport) domainHits(domain string) int { - for u, hits := range t.hits { - if u == domain { - return hits - } - } - return -1 -} - -// price example string in a JSON-like format -var priceExample string = fmt.Sprintf(`[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "%d-%02d-%02dT%02d:00:00+01:00", - "time_end": "2025-01-06T04:00:00+01:00" - }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour(), -) - -// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). -// It prevents the request from being sent over the network and count how many times -// a domain was requested. - -func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - t.hits[req.URL.Hostname()] += 1 - t.resp.Request = req - return t.resp, nil -} - -//////////////////////////////////////////////////////////////////////////////// - -const apiDomain string = "www.elprisetjustnu.se" - -func TestAPIDataFetchPeriod(t *testing.T) { - want := 3600 - if apiFetchPeriod < want { - t.Errorf("expected API fetch period >= %d, got %d", want, apiFetchPeriod) - } -} - -func TestSingleUnitAssetOneAPICall(t *testing.T) { - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - //Body: io.NopCloser(strings.NewReader(fakeBody)), - } - trans := newMockTransport(resp) - // Creates a single UnitAsset and assert it only sends a single API request - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - - // TEST CASE: cause a single API request - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests", hits) - } -} - -func TestMultipleUnitAssetOneAPICall(t *testing.T) { - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - } - trans := newMockTransport(resp) - // Creates multiple UnitAssets and monitor their API requests - units := 10 - for i := 0; i < units; i++ { - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - } - - // TEST CASE: causing only one API hit while using multiple UnitAssets - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests (from %d units)", hits, units) - } -} - -func TestSetmethods(t *testing.T) { - - asset := initTemplate().(*UnitAsset) - - // Simulate the input signals - MinTemp_inputSignal := forms.SignalA_v1a{ - Value: 1.0, - } - MaxTemp_inputSignal := forms.SignalA_v1a{ - Value: 29.0, - } - MinPrice_inputSignal := forms.SignalA_v1a{ - Value: 2.0, - } - MaxPrice_inputSignal := forms.SignalA_v1a{ - Value: 12.0, - } - DesTemp_inputSignal := forms.SignalA_v1a{ - Value: 23.7, - } - - //call and test min_temp - asset.setMin_temp(MinTemp_inputSignal) - if asset.Min_temp != 1.0 { - t.Errorf("expected Min_temp to be 1.0, got %f", asset.Min_temp) - } - // call and test max_temp - asset.setMax_temp(MaxTemp_inputSignal) - if asset.Max_temp != 29.0 { - t.Errorf("expected Max_temp to be 25.0, got %f", asset.Max_temp) - } - //call and test Min_price - asset.setMin_price(MinPrice_inputSignal) - if asset.Min_price != 2.0 { - t.Errorf("expected Min_Price to be 2.0, got %f", asset.Min_price) - } - //call and test Max_price - asset.setMax_price(MaxPrice_inputSignal) - if asset.Max_price != 12.0 { - t.Errorf("expected Max_Price to be 12.0, got %f", asset.Max_price) - } - // call and test Desired_temp - asset.setDesired_temp(DesTemp_inputSignal) - if asset.Desired_temp != 23.7 { - t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.Desired_temp) - } - -} - -func Test_GetMethods(t *testing.T) { - - uasset := initTemplate().(*UnitAsset) - - ////MinTemp//// - // check if the value from the struct is the acctual value that the func is getting - result := uasset.getMin_temp() - if result.Value != uasset.Min_temp { - t.Errorf("expected Value of the min_temp is to be %v, got %v", uasset.Min_temp, result.Value) - } - //check that the Unit is correct - if result.Unit != "Celsius" { - t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) - - } - ////MaxTemp//// - result2 := uasset.getMax_temp() - if result2.Value != uasset.Max_temp { - t.Errorf("expected Value of the Max_temp is to be %v, got %v", uasset.Max_temp, result2.Value) - } - //check that the Unit is correct - if result2.Unit != "Celsius" { - t.Errorf("expected Unit of the Max_temp is to be 'Celsius', got %v", result2.Unit) - } - ////MinPrice//// - // check if the value from the struct is the acctual value that the func is getting - result3 := uasset.getMin_price() - if result3.Value != uasset.Min_price { - t.Errorf("expected Value of the minPrice is to be %v, got %v", uasset.Min_price, result3.Value) - } - //check that the Unit is correct - if result3.Unit != "SEK" { - t.Errorf("expected Unit to be 'SEK', got %v", result3.Unit) - } - - ////MaxPrice//// - // check if the value from the struct is the acctual value that the func is getting - result4 := uasset.getMax_price() - if result4.Value != uasset.Max_price { - t.Errorf("expected Value of the maxPrice is to be %v, got %v", uasset.Max_price, result4.Value) - } - //check that the Unit is correct - if result4.Unit != "SEK" { - t.Errorf("expected Unit to be 'SEK', got %v", result4.Unit) - } - ////DesierdTemp//// - // check if the value from the struct is the acctual value that the func is getting - result5 := uasset.getDesired_temp() - if result5.Value != uasset.Desired_temp { - t.Errorf("expected desired temprature is to be %v, got %v", uasset.Desired_temp, result5.Value) - } - //check that the Unit is correct - if result5.Unit != "Celsius" { - t.Errorf("expected Unit to be 'Celsius', got %v", result5.Unit) - } - ////SEK_Price//// - result6 := uasset.getSEK_price() - if result6.Value != uasset.SEK_price { - t.Errorf("expected electric price is to be %v, got %v", uasset.SEK_price, result6.Value) - } -} - -func Test_initTemplet(t *testing.T) { - uasset := initTemplate().(*UnitAsset) - /* - name := uasset.GetName() - Services := uasset.GetServices() - Cervices := uasset.GetCervices() - Details := uasset.GetDetails() - */ - - //// unnecessary test, but good for practicing - - name := uasset.GetName() - if name != "Set Values" { - t.Errorf("expected name of the resource is %v, got %v", uasset.Name, name) - } - Services := uasset.GetServices() - if Services == nil { - t.Fatalf("If Services is nil, not worth to continue testing") - } - //Services// - if Services["SEK_price"].Definition != "SEK_price" { - t.Errorf("expected service defenition to be SEKprice") - } - if Services["max_temperature"].Definition != "max_temperature" { - t.Errorf("expected service defenition to be max_temperature") - } - if Services["min_temperature"].Definition != "min_temperature" { - t.Errorf("expected service defenition to be min_temperature") - } - if Services["max_price"].Definition != "max_price" { - t.Errorf("expected service defenition to be max_price") - } - if Services["min_price"].Definition != "min_price" { - t.Errorf("expected service defenition to be min_price") - } - if Services["desired_temp"].Definition != "desired_temp" { - t.Errorf("expected service defenition to be desired_temp") - } - //GetCervice// - Cervices := uasset.GetCervices() - if Cervices != nil { - t.Fatalf("If cervises not nil, not worth to continue testing") - } - //Testing Details// - Details := uasset.GetDetails() - if Details == nil { - t.Errorf("expected a map, but Details was nil, ") - } - -} - -func Test_newUnitAsset(t *testing.T) { - // prepare for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled - defer cancel() // make sure all paths cancel the context to avoid context leak - - // instantiate the System - sys := components.NewSystem("Comfortstat", ctx) - - // Instatiate the Capusle - sys.Husk = &components.Husk{ - Description: " is a controller for a consumed servo motor position based on a consumed temperature", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, - InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", - } - setSEK_price := components.Service{ - Definition: "SEK_price", - SubPath: "SEK_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the current electric hourly price (using a GET request)", - } - - setMax_temp := components.Service{ - Definition: "max_temperature", - SubPath: "max_temperature", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the maximum temp the user wants (using a GET request)", - } - setMin_temp := components.Service{ - Definition: "min_temperature", - SubPath: "min_temperature", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum temp the user could tolerate (using a GET request)", - } - setMax_price := components.Service{ - Definition: "max_price", - SubPath: "max_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the maximum price the user wants to pay (using a GET request)", - } - setMin_price := components.Service{ - Definition: "min_price", - SubPath: "min_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum price the user wants to pay (using a GET request)", - } - setDesired_temp := components.Service{ - Definition: "desired_temp", - SubPath: "desired_temp", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", - } - - uac := UnitAsset{ - //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", - Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 1.5, // Example electricity price in SEK per kWh - Min_price: 1.0, // Minimum price allowed - Max_price: 2.0, // Maximum price allowed - Min_temp: 20.0, // Minimum temperature - Max_temp: 25.0, // Maximum temprature allowed - Desired_temp: 0, // Desired temp calculated by system - Period: 15, - - // maps the provided services from above - ServicesMap: components.Services{ - setMax_temp.SubPath: &setMax_temp, - setMin_temp.SubPath: &setMin_temp, - setMax_price.SubPath: &setMax_price, - setMin_price.SubPath: &setMin_price, - setSEK_price.SubPath: &setSEK_price, - setDesired_temp.SubPath: &setDesired_temp, - }, - } - - ua, _ := newUnitAsset(uac, &sys, nil) - - name := ua.GetName() - if name != "Set Values" { - t.Errorf("expected name to be Set values, but got: %v", name) - } - -} - -func Test_calculateDesiredTemp(t *testing.T) { - var True_result float64 = 22.5 - asset := initTemplate().(*UnitAsset) - - result := asset.calculateDesiredTemp() - - if result != True_result { - t.Errorf("Expected calculated temp is %v, got %v", True_result, result) - } -} - -func Test_specialcalculate(t *testing.T) { - asset := UnitAsset{ - SEK_price: 3.0, - Max_price: 2.0, - Min_temp: 17.0, - } - - result := asset.calculateDesiredTemp() - - if result != asset.Min_temp { - t.Errorf("Expected temperature to be %v, got %v", asset.Min_temp, result) - } -} - -// Fuctions that help creating bad body -type errReader int - -var errBodyRead error = fmt.Errorf("bad body read") - -func (errReader) Read(p []byte) (n int, err error) { - return 0, errBodyRead -} - -func (errReader) Close() error { - return nil -} - -var brokenURL string = string([]byte{0x7f}) - -func TestGetAPIPriceData(t *testing.T) { - priceExample = fmt.Sprintf(`[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "%d-%02d-%02dT%02d:00:00+01:00", - "time_end": "2025-01-06T04:00:00+01:00" - }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour(), - ) - - fakeBody := fmt.Sprintf(priceExample) - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - - // Testing good cases - - // Test case: goal is no errors - url := fmt.Sprintf( - `https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, - time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), - ) - newMockTransport(resp) - err := getAPIPriceData(url) - if err != nil { - t.Errorf("expected no errors but got %s :", err) - } - - // Check if the correct price is stored - expectedPrice := 0.26673 - - if globalPrice.SEK_price != expectedPrice { - t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) - } - - // Testing bad cases - - // Test case: using wrong url leads to an error - newMockTransport(resp) - // Call the function (which now hits the mock server) - err = getAPIPriceData(brokenURL) - if err == nil { - t.Errorf("Expected an error but got none!") - } - - // Test case: if reading the body causes an error - resp.Body = errReader(0) - newMockTransport(resp) - err = getAPIPriceData(url) - if err != errBodyRead { - t.Errorf("expected an error %v, got %v", errBodyRead, err) - } - - //Test case: if status code > 299 - resp.Body = io.NopCloser(strings.NewReader(fakeBody)) - resp.StatusCode = 300 - newMockTransport(resp) - err = getAPIPriceData(url) - - if err != err_statuscode { - t.Errorf("expected an bad status code but got %v", err) - - } - - // test case: if unmarshal a bad body creates a error - resp.StatusCode = 200 - resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) - newMockTransport(resp) - err = getAPIPriceData(url) - - if err == nil { - t.Errorf("expected an error, got %v :", err) - } - -} From 896b92ace2f057ad4e4466570f7dc76eb7e12d8f Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 14:06:15 +0100 Subject: [PATCH 073/102] changed to a more suitable name --- Comfortstat/{api_fetch_test.go => things_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Comfortstat/{api_fetch_test.go => things_test.go} (100%) diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/things_test.go similarity index 100% rename from Comfortstat/api_fetch_test.go rename to Comfortstat/things_test.go From 750739fde5479a81141983e51bd970724717c679 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Wed, 5 Feb 2025 09:57:31 +0100 Subject: [PATCH 074/102] replaced the sleep in things.go to Comfortstat.go --- Comfortstat/Comfortstat.go | 1 + Comfortstat/things.go | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index 3663aa3..896f462 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -32,6 +32,7 @@ func main() { // instantiate a template unit asset assetTemplate := initTemplate() initAPI() + time.Sleep(1 * time.Second) assetName := assetTemplate.GetName() sys.UAssets[assetName] = &assetTemplate diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 2195f6f..bc93474 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -374,9 +374,6 @@ func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { } func retrieveAPI_price(ua *UnitAsset) { - if globalPrice.SEK_price == 0 { - time.Sleep(1 * time.Second) - } ua.SEK_price = globalPrice.SEK_price // Don't send temperature updates if the difference is too low // (this could potentially save on battery!) From 9f2aec871da7101e2b9742ba1438483a645e5e1f Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Fri, 7 Feb 2025 14:15:06 +0100 Subject: [PATCH 075/102] Added user temperature to the comfortstat --- Comfortstat/Comfortstat.go | 19 +++++++++++++++ Comfortstat/things.go | 48 +++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index 896f462..c3c77cd 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -83,6 +83,8 @@ func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath t.set_SEKprice(w, r) case "desired_temp": t.set_desiredTemp(w, r) + case "userTemp": + t.set_userTemp(w, r) default: http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) } @@ -193,3 +195,20 @@ func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { } } + +func (rsc *UnitAsset) set_userTemp(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "PUT": + sig, err := usecases.HTTPProcessSetRequest(w, r) + if err != nil { + http.Error(w, "request incorrectly formated", http.StatusBadRequest) + return + } + rsc.setUser_Temp(sig) + case "GET": + signalErr := rsc.getUser_Temp() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/Comfortstat/things.go b/Comfortstat/things.go index bc93474..c9e42e6 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -52,6 +52,7 @@ type UnitAsset struct { Max_price float64 `json:"max_price"` Min_temp float64 `json:"min_temp"` Max_temp float64 `json:"max_temp"` + userTemp float64 `json:"userTemp"` } func initAPI() { @@ -188,6 +189,12 @@ func initTemplate() components.UnitAsset { Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", } + setUserTemp := components.Service{ + Definition: "userTemp", + SubPath: "userTemp", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the temperature the user wants regardless of prices (using a GET request)", + } return &UnitAsset{ //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) @@ -200,6 +207,7 @@ func initTemplate() components.UnitAsset { Max_temp: 25.0, // Maximum temprature allowed Desired_temp: 0, // Desired temp calculated by system Period: 15, + userTemp: 0, // maps the provided services from above ServicesMap: components.Services{ @@ -209,6 +217,7 @@ func initTemplate() components.UnitAsset { setMin_price.SubPath: &setMin_price, setSEK_price.SubPath: &setSEK_price, setDesired_temp.SubPath: &setDesired_temp, + setUserTemp.SubPath: &setUserTemp, }, } } @@ -244,6 +253,7 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv Max_temp: uac.Max_temp, Desired_temp: uac.Desired_temp, Period: uac.Period, + userTemp: uac.userTemp, CervicesMap: components.Cervices{ t.Name: t, }, @@ -290,6 +300,7 @@ func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { // setMin_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { ua.Min_price = f.Value + ua.processFeedbackLoop() } // getMax_price is used for reading the current value of Max_price @@ -304,6 +315,7 @@ func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { // setMax_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { ua.Max_price = f.Value + ua.processFeedbackLoop() } // getMin_temp is used for reading the current minimum temerature value @@ -318,6 +330,7 @@ func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { // setMin_temp updates the current minimum temperature set by the user with a new value func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { ua.Min_temp = f.Value + ua.processFeedbackLoop() } // getMax_temp is used for reading the current value of Min_price @@ -332,6 +345,7 @@ func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { // setMax_temp updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { ua.Max_temp = f.Value + ua.processFeedbackLoop() } func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { @@ -347,6 +361,21 @@ func (ua *UnitAsset) setDesired_temp(f forms.SignalA_v1a) { log.Printf("new desired temperature: %.1f", f.Value) } +func (ua *UnitAsset) setUser_Temp(f forms.SignalA_v1a) { + ua.userTemp = f.Value + if ua.userTemp != 0 { + ua.sendUserTemp() + } +} + +func (ua *UnitAsset) getUser_Temp() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.userTemp + f.Unit = "Celsius" + f.Timestamp = time.Now() + return f +} + // NOTE// // It's _strongly_ encouraged to not send requests to the API for more than once per hour. // Making this period a private constant prevents a user from changing this value @@ -408,7 +437,7 @@ func (ua *UnitAsset) processFeedbackLoop() { //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) ua.Desired_temp = ua.calculateDesiredTemp() // Only send temperature update when we have a new value. - if ua.Desired_temp == ua.old_desired_temp { + if (ua.Desired_temp == ua.old_desired_temp) || (ua.userTemp != 0) { return } // Keep track of previous value @@ -452,3 +481,20 @@ func (ua *UnitAsset) calculateDesiredTemp() float64 { return desired_temp } + +func (ua *UnitAsset) sendUserTemp() { + var of forms.SignalA_v1a + of.Value = ua.userTemp + of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] + of.Timestamp = time.Now() + + op, err := usecases.Pack(&of, "application/json") + if err != nil { + return + } + err = usecases.SetState(ua.CervicesMap["setpoint"], ua.Owner, op) + if err != nil { + log.Printf("cannot update zigbee setpoint: %s\n", err) + return + } +} From f5734c9b803ebe155733f0e3d365d760845f2ac3 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 7 Feb 2025 15:12:03 +0100 Subject: [PATCH 076/102] added some comments and removed emty lines --- Comfortstat/Comfortstat.go | 1 + Comfortstat/Comfortstat_test.go | 95 +++++---------------------------- Comfortstat/things.go | 9 ++-- 3 files changed, 19 insertions(+), 86 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index c3c77cd..13796ad 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -31,6 +31,7 @@ func main() { // instantiate a template unit asset assetTemplate := initTemplate() + // Calling initAPI() starts the pricefeedbackloop that fetches the current electrisity price for the particular hour initAPI() time.Sleep(1 * time.Second) assetName := assetTemplate.GetName() diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index ebf4960..268064c 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -16,23 +16,20 @@ func Test_set_SEKprice(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/SEK_price", nil) good_code := 200 - ua.set_SEKprice(w, r) - + // calls the method and extracts the response and save is in resp for the upcoming tests resp := w.Result() if resp.StatusCode != good_code { t.Errorf("expected good status code: %v, got %v", good_code, resp.StatusCode) } - body, _ := io.ReadAll(resp.Body) - + // this is a simple check if the JSON response contains the specific value/unit/version value := strings.Contains(string(body), `"value": 1.5`) unit := strings.Contains(string(body), `"unit": "SEK"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - + // check results from above if value != true { t.Errorf("expected the statment to be true!") - } if unit != true { t.Errorf("expected the unit statement to be true!") @@ -43,15 +40,12 @@ func Test_set_SEKprice(t *testing.T) { // Bad test case: default part of code w = httptest.NewRecorder() r = httptest.NewRequest("123", "http://localhost:8670/Comfortstat/Set%20Values/SEK_price", nil) - + // calls the method and extracts the response and save is in resp for the upcoming tests ua.set_SEKprice(w, r) - resp = w.Result() - if resp.StatusCode != http.StatusNotFound { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } - } func Test_set_minTemp(t *testing.T) { @@ -66,7 +60,6 @@ func Test_set_minTemp(t *testing.T) { r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 - ua.set_minTemp(w, r) // save the rsponse and read the body @@ -82,60 +75,47 @@ func Test_set_minTemp(t *testing.T) { fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_minTemp(w, r) - // save the rsponse and read the body resp = w.Result() if resp.StatusCode == good_statuscode { t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) } - //Good test case: GET - w = httptest.NewRecorder() r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) good_statuscode = 200 ua.set_minTemp(w, r) // save the rsponse and read the body - resp = w.Result() if resp.StatusCode != good_statuscode { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) - + // this is a simple check if the JSON response contains the specific value/unit/version value := strings.Contains(string(body), `"value": 20`) unit := strings.Contains(string(body), `"unit": "Celsius"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - + // check the result from above if value != true { t.Errorf("expected the statment to be true!") - } - if unit != true { t.Errorf("expected the unit statement to be true!") } - if version != true { t.Errorf("expected the version statment to be true!") } - // bad test case: default part of code // force the case to hit default statement but alter the method w = httptest.NewRecorder() r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) - ua.set_minTemp(w, r) - resp = w.Result() - if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) - } } @@ -149,7 +129,6 @@ func Test_set_maxTemp(t *testing.T) { r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 - ua.set_maxTemp(w, r) // save the rsponse and read the body @@ -157,7 +136,6 @@ func Test_set_maxTemp(t *testing.T) { if resp.StatusCode != good_statuscode { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } - //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data @@ -165,7 +143,6 @@ func Test_set_maxTemp(t *testing.T) { fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_maxTemp(w, r) // save the rsponse and read the body @@ -174,7 +151,6 @@ func Test_set_maxTemp(t *testing.T) { t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) } //Good test case: GET - w = httptest.NewRecorder() r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) good_statuscode = 200 @@ -186,26 +162,20 @@ func Test_set_maxTemp(t *testing.T) { if resp.StatusCode != good_statuscode { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } - body, _ := io.ReadAll(resp.Body) - + // this is a simple check if the JSON response contains the specific value/unit/version value := strings.Contains(string(body), `"value": 25`) unit := strings.Contains(string(body), `"unit": "Celsius"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if value != true { t.Errorf("expected the statment to be true!") - } - if unit != true { t.Errorf("expected the unit statement to be true!") } - if version != true { t.Errorf("expected the version statment to be true!") } - // bad test case: default part of code // force the case to hit default statement but alter the method @@ -213,12 +183,9 @@ func Test_set_maxTemp(t *testing.T) { r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) ua.set_maxTemp(w, r) - resp = w.Result() - if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) - } } @@ -232,7 +199,6 @@ func Test_set_minPrice(t *testing.T) { r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/min_price", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 - ua.set_minPrice(w, r) // save the rsponse and read the body @@ -240,7 +206,6 @@ func Test_set_minPrice(t *testing.T) { if resp.StatusCode != good_statuscode { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } - //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data @@ -248,59 +213,47 @@ func Test_set_minPrice(t *testing.T) { fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/min_price", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_minPrice(w, r) - - // save the rsponse and read the body + // save the rsponse resp = w.Result() if resp.StatusCode == good_statuscode { t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) } //Good test case: GET - w = httptest.NewRecorder() r = httptest.NewRequest("GET", "localhost:8670/Comfortstat/Set%20Values/min_price", nil) good_statuscode = 200 ua.set_minPrice(w, r) // save the rsponse and read the body - resp = w.Result() if resp.StatusCode != good_statuscode { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) - - value := strings.Contains(string(body), `"value": 1`) //EVENTUELL BUGG, enligt webb-app minPrice = 0 ( kanske dock är för att jag inte startat sregistrar och orchastrator) + // this is a simple check if the JSON response contains the specific value/unit/version + value := strings.Contains(string(body), `"value": 1`) unit := strings.Contains(string(body), `"unit": "SEK"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) - if value != true { t.Errorf("expected the statment to be true!") - } - if unit != true { t.Errorf("expected the unit statement to be true!") } - if version != true { t.Errorf("expected the version statment to be true!") } - // bad test case: default part of code // force the case to hit default statement but alter the method w = httptest.NewRecorder() r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/min_price", nil) - ua.set_minPrice(w, r) - + //save the response resp = w.Result() - if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) - } } @@ -314,7 +267,6 @@ func Test_set_maxPrice(t *testing.T) { r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/max_price", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. good_statuscode := 200 - ua.set_maxPrice(w, r) // save the rsponse and read the body @@ -322,7 +274,6 @@ func Test_set_maxPrice(t *testing.T) { if resp.StatusCode != good_statuscode { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } - //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data @@ -330,7 +281,6 @@ func Test_set_maxPrice(t *testing.T) { fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/max_price", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_maxPrice(w, r) // save the rsponse and read the body @@ -339,37 +289,31 @@ func Test_set_maxPrice(t *testing.T) { t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) } //Good test case: GET - w = httptest.NewRecorder() r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) good_statuscode = 200 ua.set_maxPrice(w, r) // save the rsponse and read the body - resp = w.Result() if resp.StatusCode != good_statuscode { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) - + // this is a simple check if the JSON response contains the specific value/unit/version value := strings.Contains(string(body), `"value": 2`) unit := strings.Contains(string(body), `"unit": "SEK"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { t.Errorf("expected the statment to be true!") - } - if unit != true { t.Errorf("expected the unit statement to be true!") } - if version != true { t.Errorf("expected the version statment to be true!") } - // bad test case: default part of code // force the case to hit default statement but alter the method @@ -377,12 +321,10 @@ func Test_set_maxPrice(t *testing.T) { r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) ua.set_maxPrice(w, r) - resp = w.Result() if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) - } } @@ -414,14 +356,12 @@ func Test_set_desiredTemp(t *testing.T) { r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. ua.set_desiredTemp(w, r) - // save the rsponse and read the body resp = w.Result() if resp.StatusCode == good_statuscode { t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) } //Good test case: GET - w = httptest.NewRecorder() r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) good_statuscode = 200 @@ -434,36 +374,29 @@ func Test_set_desiredTemp(t *testing.T) { t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) - + // this is a simple check if the JSON response contains the specific value/unit/version value := strings.Contains(string(body), `"value": 0`) unit := strings.Contains(string(body), `"unit": "Celsius"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { t.Errorf("expected the statment to be true!") - } - if unit != true { t.Errorf("expected the unit statement to be true!") } - if version != true { t.Errorf("expected the version statment to be true!") } - // bad test case: default part of code // force the case to hit default statement but alter the method w = httptest.NewRecorder() r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) - + // calls the method and extracts the response and save is in resp for the upcoming tests ua.set_desiredTemp(w, r) - resp = w.Result() - if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) - } } diff --git a/Comfortstat/things.go b/Comfortstat/things.go index c9e42e6..4dabbf2 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -158,7 +158,6 @@ func initTemplate() components.UnitAsset { Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current electric hourly price (using a GET request)", } - setMax_temp := components.Service{ Definition: "max_temperature", SubPath: "max_temperature", @@ -300,7 +299,7 @@ func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { // setMin_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { ua.Min_price = f.Value - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } // getMax_price is used for reading the current value of Max_price @@ -315,7 +314,7 @@ func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { // setMax_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { ua.Max_price = f.Value - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } // getMin_temp is used for reading the current minimum temerature value @@ -330,7 +329,7 @@ func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { // setMin_temp updates the current minimum temperature set by the user with a new value func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { ua.Min_temp = f.Value - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } // getMax_temp is used for reading the current value of Min_price @@ -345,7 +344,7 @@ func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { // setMax_temp updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { ua.Max_temp = f.Value - ua.processFeedbackLoop() + //ua.processFeedbackLoop() } func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { From 3150c3c74bab91d4962f87bcc2a0d9c625d5e3ea Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 7 Feb 2025 15:52:52 +0100 Subject: [PATCH 077/102] fixed the setter-methods --- Comfortstat/things.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 4dabbf2..d4710f5 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -299,7 +299,6 @@ func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { // setMin_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { ua.Min_price = f.Value - //ua.processFeedbackLoop() } // getMax_price is used for reading the current value of Max_price @@ -314,7 +313,6 @@ func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { // setMax_price updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { ua.Max_price = f.Value - //ua.processFeedbackLoop() } // getMin_temp is used for reading the current minimum temerature value @@ -329,7 +327,6 @@ func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { // setMin_temp updates the current minimum temperature set by the user with a new value func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { ua.Min_temp = f.Value - //ua.processFeedbackLoop() } // getMax_temp is used for reading the current value of Min_price @@ -344,7 +341,6 @@ func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { // setMax_temp updates the current minimum price set by the user with a new value func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { ua.Max_temp = f.Value - //ua.processFeedbackLoop() } func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { From 07796b99cf8cf91d05e8d964477a5750148b5625 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Mon, 10 Feb 2025 10:39:42 +0100 Subject: [PATCH 078/102] Added special case for the user temp --- Comfortstat/things.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index d4710f5..3812a82 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -433,6 +433,10 @@ func (ua *UnitAsset) processFeedbackLoop() { ua.Desired_temp = ua.calculateDesiredTemp() // Only send temperature update when we have a new value. if (ua.Desired_temp == ua.old_desired_temp) || (ua.userTemp != 0) { + if ua.userTemp != 0 { + ua.old_desired_temp = ua.userTemp + return + } return } // Keep track of previous value From 68988ceeb5d6a99a248d9d8aee1cc96bae601755 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Mon, 10 Feb 2025 11:15:06 +0100 Subject: [PATCH 079/102] Changed userTemp to UserTemp so it is exported as json --- Comfortstat/things.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 3812a82..95483d4 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -52,7 +52,7 @@ type UnitAsset struct { Max_price float64 `json:"max_price"` Min_temp float64 `json:"min_temp"` Max_temp float64 `json:"max_temp"` - userTemp float64 `json:"userTemp"` + UserTemp float64 `json:"userTemp"` } func initAPI() { @@ -206,7 +206,7 @@ func initTemplate() components.UnitAsset { Max_temp: 25.0, // Maximum temprature allowed Desired_temp: 0, // Desired temp calculated by system Period: 15, - userTemp: 0, + UserTemp: 0, // maps the provided services from above ServicesMap: components.Services{ @@ -252,7 +252,7 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv Max_temp: uac.Max_temp, Desired_temp: uac.Desired_temp, Period: uac.Period, - userTemp: uac.userTemp, + UserTemp: uac.UserTemp, CervicesMap: components.Cervices{ t.Name: t, }, @@ -357,15 +357,15 @@ func (ua *UnitAsset) setDesired_temp(f forms.SignalA_v1a) { } func (ua *UnitAsset) setUser_Temp(f forms.SignalA_v1a) { - ua.userTemp = f.Value - if ua.userTemp != 0 { + ua.UserTemp = f.Value + if ua.UserTemp != 0 { ua.sendUserTemp() } } func (ua *UnitAsset) getUser_Temp() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.userTemp + f.Value = ua.UserTemp f.Unit = "Celsius" f.Timestamp = time.Now() return f @@ -432,9 +432,9 @@ func (ua *UnitAsset) processFeedbackLoop() { //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) ua.Desired_temp = ua.calculateDesiredTemp() // Only send temperature update when we have a new value. - if (ua.Desired_temp == ua.old_desired_temp) || (ua.userTemp != 0) { - if ua.userTemp != 0 { - ua.old_desired_temp = ua.userTemp + if (ua.Desired_temp == ua.old_desired_temp) || (ua.UserTemp != 0) { + if ua.UserTemp != 0 { + ua.old_desired_temp = ua.UserTemp return } return @@ -483,7 +483,7 @@ func (ua *UnitAsset) calculateDesiredTemp() float64 { func (ua *UnitAsset) sendUserTemp() { var of forms.SignalA_v1a - of.Value = ua.userTemp + of.Value = ua.UserTemp of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] of.Timestamp = time.Now() From 2679a32fbd73f8b3bef10b76d6baa6f5bd1f0911 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 11 Feb 2025 08:58:57 +0100 Subject: [PATCH 080/102] cleaned up some emty rows and created some explanatory comments --- Comfortstat/Comfortstat_test.go | 3 -- Comfortstat/things_test.go | 57 +++++++++------------------------ 2 files changed, 15 insertions(+), 45 deletions(-) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index 268064c..fd2ae60 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -49,11 +49,9 @@ func Test_set_SEKprice(t *testing.T) { } func Test_set_minTemp(t *testing.T) { - ua := initTemplate().(*UnitAsset) //Godd test case: PUT - // creates a fake request body with JSON data w := httptest.NewRecorder() fakebody := bytes.NewReader([]byte(`{"value": 20, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read @@ -108,7 +106,6 @@ func Test_set_minTemp(t *testing.T) { t.Errorf("expected the version statment to be true!") } // bad test case: default part of code - // force the case to hit default statement but alter the method w = httptest.NewRecorder() r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go index b91fc84..439b554 100644 --- a/Comfortstat/things_test.go +++ b/Comfortstat/things_test.go @@ -32,7 +32,6 @@ func newMockTransport(resp *http.Response) mockTransport { } // domainHits returns the number of requests to a domain (or -1 if domain wasn't found). - func (t mockTransport) domainHits(domain string) int { for u, hits := range t.hits { if u == domain { @@ -55,15 +54,13 @@ var priceExample string = fmt.Sprintf(`[{ // RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). // It prevents the request from being sent over the network and count how many times // a domain was requested. - func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { t.hits[req.URL.Hostname()] += 1 t.resp.Request = req return t.resp, nil } -//////////////////////////////////////////////////////////////////////////////// - +// ////////////////////////////////////////////////////////////////////////////// const apiDomain string = "www.elprisetjustnu.se" func TestAPIDataFetchPeriod(t *testing.T) { @@ -103,7 +100,6 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { ua := initTemplate().(*UnitAsset) retrieveAPI_price(ua) } - // TEST CASE: causing only one API hit while using multiple UnitAssets hits := trans.domainHits(apiDomain) if hits > 1 { @@ -112,7 +108,6 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { } func TestSetmethods(t *testing.T) { - asset := initTemplate().(*UnitAsset) // Simulate the input signals @@ -131,7 +126,6 @@ func TestSetmethods(t *testing.T) { DesTemp_inputSignal := forms.SignalA_v1a{ Value: 23.7, } - //call and test min_temp asset.setMin_temp(MinTemp_inputSignal) if asset.Min_temp != 1.0 { @@ -157,11 +151,9 @@ func TestSetmethods(t *testing.T) { if asset.Desired_temp != 23.7 { t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.Desired_temp) } - } func Test_GetMethods(t *testing.T) { - uasset := initTemplate().(*UnitAsset) ////MinTemp//// @@ -173,7 +165,6 @@ func Test_GetMethods(t *testing.T) { //check that the Unit is correct if result.Unit != "Celsius" { t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) - } ////MaxTemp//// result2 := uasset.getMax_temp() @@ -194,7 +185,6 @@ func Test_GetMethods(t *testing.T) { if result3.Unit != "SEK" { t.Errorf("expected Unit to be 'SEK', got %v", result3.Unit) } - ////MaxPrice//// // check if the value from the struct is the acctual value that the func is getting result4 := uasset.getMax_price() @@ -224,15 +214,8 @@ func Test_GetMethods(t *testing.T) { func Test_initTemplet(t *testing.T) { uasset := initTemplate().(*UnitAsset) - /* - name := uasset.GetName() - Services := uasset.GetServices() - Cervices := uasset.GetCervices() - Details := uasset.GetDetails() - */ //// unnecessary test, but good for practicing - name := uasset.GetName() if name != "Set Values" { t.Errorf("expected name of the resource is %v, got %v", uasset.Name, name) @@ -270,14 +253,12 @@ func Test_initTemplet(t *testing.T) { if Details == nil { t.Errorf("expected a map, but Details was nil, ") } - } func Test_newUnitAsset(t *testing.T) { // prepare for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled defer cancel() // make sure all paths cancel the context to avoid context leak - // instantiate the System sys := components.NewSystem("Comfortstat", ctx) @@ -295,7 +276,6 @@ func Test_newUnitAsset(t *testing.T) { Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current electric hourly price (using a GET request)", } - setMax_temp := components.Service{ Definition: "max_temperature", SubPath: "max_temperature", @@ -326,7 +306,7 @@ func Test_newUnitAsset(t *testing.T) { Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", } - + // new Unitasset struct init. uac := UnitAsset{ //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) Name: "Set Values", @@ -351,34 +331,35 @@ func Test_newUnitAsset(t *testing.T) { } ua, _ := newUnitAsset(uac, &sys, nil) - + // Calls the method that gets the name of the new unitasset. name := ua.GetName() if name != "Set Values" { t.Errorf("expected name to be Set values, but got: %v", name) } - } +// Test if the method calculateDesierdTemp() calculates a correct value func Test_calculateDesiredTemp(t *testing.T) { var True_result float64 = 22.5 asset := initTemplate().(*UnitAsset) - + // calls and saves the value result := asset.calculateDesiredTemp() - + // checks if actual calculated value matches the expexted value if result != True_result { t.Errorf("Expected calculated temp is %v, got %v", True_result, result) } } +// This test catches the special cases, when the temprature is to be set to the minimum temprature right away func Test_specialcalculate(t *testing.T) { asset := UnitAsset{ SEK_price: 3.0, Max_price: 2.0, Min_temp: 17.0, } - + //call the method and save the result in a varable for testing result := asset.calculateDesiredTemp() - + //check the result from the call above if result != asset.Min_temp { t.Errorf("Expected temperature to be %v, got %v", asset.Min_temp, result) } @@ -397,9 +378,11 @@ func (errReader) Close() error { return nil } +// cretas a URL that is broken var brokenURL string = string([]byte{0x7f}) func TestGetAPIPriceData(t *testing.T) { + // creating a price example, nessasry fore the test priceExample = fmt.Sprintf(`[{ "SEK_per_kWh": 0.26673, "EUR_per_kWh": 0.02328, @@ -408,36 +391,31 @@ func TestGetAPIPriceData(t *testing.T) { "time_end": "2025-01-06T04:00:00+01:00" }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour(), ) - + // creates a fake response fakeBody := fmt.Sprintf(priceExample) resp := &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(strings.NewReader(fakeBody)), } - // Testing good cases - // Test case: goal is no errors url := fmt.Sprintf( `https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), ) + // creates a mock HTTP transport to simulate api respone for the test newMockTransport(resp) err := getAPIPriceData(url) if err != nil { t.Errorf("expected no errors but got %s :", err) } - // Check if the correct price is stored expectedPrice := 0.26673 - if globalPrice.SEK_price != expectedPrice { t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) } - // Testing bad cases - // Test case: using wrong url leads to an error newMockTransport(resp) // Call the function (which now hits the mock server) @@ -445,7 +423,6 @@ func TestGetAPIPriceData(t *testing.T) { if err == nil { t.Errorf("Expected an error but got none!") } - // Test case: if reading the body causes an error resp.Body = errReader(0) newMockTransport(resp) @@ -453,26 +430,22 @@ func TestGetAPIPriceData(t *testing.T) { if err != errBodyRead { t.Errorf("expected an error %v, got %v", errBodyRead, err) } - //Test case: if status code > 299 resp.Body = io.NopCloser(strings.NewReader(fakeBody)) resp.StatusCode = 300 newMockTransport(resp) err = getAPIPriceData(url) - + // check the statuscode is bad, witch is expected for the test to be successful if err != err_statuscode { t.Errorf("expected an bad status code but got %v", err) - } - // test case: if unmarshal a bad body creates a error resp.StatusCode = 200 resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) newMockTransport(resp) err = getAPIPriceData(url) - + // make the check if the unmarshal creats a error if err == nil { t.Errorf("expected an error, got %v :", err) } - } From 77d7fce6b6494e0990e419c119845d6de4170c34 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 11 Feb 2025 10:49:50 +0100 Subject: [PATCH 081/102] Resolved all the comments in the review part --- Comfortstat/Comfortstat.go | 66 ++++----- Comfortstat/Comfortstat_test.go | 222 ++++++++++++++-------------- Comfortstat/things.go | 251 ++++++++++++++++---------------- Comfortstat/things_test.go | 208 +++++++++++++------------- 4 files changed, 374 insertions(+), 373 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index 13796ad..f93498c 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -72,29 +72,29 @@ func main() { // Serving handles the resources services. NOTE: it exepcts those names from the request URL path func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { switch servicePath { - case "min_temperature": - t.set_minTemp(w, r) - case "max_temperature": - t.set_maxTemp(w, r) - case "max_price": - t.set_maxPrice(w, r) - case "min_price": - t.set_minPrice(w, r) - case "SEK_price": - t.set_SEKprice(w, r) - case "desired_temp": - t.set_desiredTemp(w, r) + case "MinTemperature": + t.httpSetMinTemp(w, r) + case "MaxTemperature": + t.httpSetMaxTemp(w, r) + case "MaxPrice": + t.httpSetMaxPrice(w, r) + case "MinPrice": + t.httpSetMinPrice(w, r) + case "SEKPrice": + t.httpSetSEKPrice(w, r) + case "DesiredTemp": + t.httpSetDesiredTemp(w, r) case "userTemp": - t.set_userTemp(w, r) + t.httpSetUserTemp(w, r) default: http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) } } -func (rsc *UnitAsset) set_SEKprice(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetSEKPrice(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - signalErr := rsc.getSEK_price() + signalErr := rsc.getSEKPrice() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) @@ -104,7 +104,7 @@ func (rsc *UnitAsset) set_SEKprice(w http.ResponseWriter, r *http.Request) { // All these functions below handles HTTP "PUT" or "GET" requests to modefy or retrieve the MAX/MIN temprature/price and desierd temprature // For the PUT case - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, // call the set functions in things.go with the value witch updates the value in the struct -func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetMinTemp(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) @@ -114,15 +114,15 @@ func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { return } - rsc.setMin_temp(sig) + rsc.setMinTemp(sig) case "GET": - signalErr := rsc.getMin_temp() + signalErr := rsc.getMinTemp() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) } } -func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetMaxTemp(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) @@ -131,16 +131,16 @@ func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } - rsc.setMax_temp(sig) + rsc.setMaxTemp(sig) case "GET": - signalErr := rsc.getMax_temp() + signalErr := rsc.getMaxTemp() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) } } -func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetMinPrice(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) @@ -149,9 +149,9 @@ func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } - rsc.setMin_price(sig) + rsc.setMinPrice(sig) case "GET": - signalErr := rsc.getMin_price() + signalErr := rsc.getMinPrice() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) @@ -159,7 +159,7 @@ func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { } } -func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetMaxPrice(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) @@ -168,9 +168,9 @@ func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } - rsc.setMax_price(sig) + rsc.setMaxPrice(sig) case "GET": - signalErr := rsc.getMax_price() + signalErr := rsc.getMaxPrice() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) @@ -178,7 +178,7 @@ func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { } } -func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetDesiredTemp(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) @@ -187,9 +187,9 @@ func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { http.Error(w, "request incorreclty formated", http.StatusBadRequest) return } - rsc.setDesired_temp(sig) + rsc.setDesiredTemp(sig) case "GET": - signalErr := rsc.getDesired_temp() + signalErr := rsc.getDesiredTemp() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) @@ -197,7 +197,7 @@ func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { } -func (rsc *UnitAsset) set_userTemp(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetUserTemp(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) @@ -205,9 +205,9 @@ func (rsc *UnitAsset) set_userTemp(w http.ResponseWriter, r *http.Request) { http.Error(w, "request incorrectly formated", http.StatusBadRequest) return } - rsc.setUser_Temp(sig) + rsc.setUserTemp(sig) case "GET": - signalErr := rsc.getUser_Temp() + signalErr := rsc.getUserTemp() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index fd2ae60..88014c8 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -9,18 +9,18 @@ import ( "testing" ) -func Test_set_SEKprice(t *testing.T) { +func TestHttpSetSEKPrice(t *testing.T) { ua := initTemplate().(*UnitAsset) //Good case test: GET w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/SEK_price", nil) - good_code := 200 - ua.set_SEKprice(w, r) + r := httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/SEKPrice", nil) + goodCode := 200 + ua.httpSetSEKPrice(w, r) // calls the method and extracts the response and save is in resp for the upcoming tests resp := w.Result() - if resp.StatusCode != good_code { - t.Errorf("expected good status code: %v, got %v", good_code, resp.StatusCode) + if resp.StatusCode != goodCode { + t.Errorf("expected good status code: %v, got %v", goodCode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) // this is a simple check if the JSON response contains the specific value/unit/version @@ -39,56 +39,56 @@ func Test_set_SEKprice(t *testing.T) { } // Bad test case: default part of code w = httptest.NewRecorder() - r = httptest.NewRequest("123", "http://localhost:8670/Comfortstat/Set%20Values/SEK_price", nil) + r = httptest.NewRequest("123", "http://localhost:8670/Comfortstat/Set%20Values/SEKPrice", nil) // calls the method and extracts the response and save is in resp for the upcoming tests - ua.set_SEKprice(w, r) + ua.httpSetSEKPrice(w, r) resp = w.Result() if resp.StatusCode != http.StatusNotFound { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } } -func Test_set_minTemp(t *testing.T) { +func TestHttpSetMinTemp(t *testing.T) { ua := initTemplate().(*UnitAsset) //Godd test case: PUT // creates a fake request body with JSON data w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 20, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - good_statuscode := 200 - ua.set_minTemp(w, r) + fakebody := bytes.NewReader([]byte(`{"value": 20, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/MinTemperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + ua.httpSetMinTemp(w, r) // save the rsponse and read the body resp := w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_minTemp(w, r) + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/MinTemperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetMinTemp(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode == good_statuscode { - t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) } //Good test case: GET w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) - good_statuscode = 200 - ua.set_minTemp(w, r) + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/MinTemperature", nil) + goodStatusCode = 200 + ua.httpSetMinTemp(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) // this is a simple check if the JSON response contains the specific value/unit/version @@ -108,56 +108,56 @@ func Test_set_minTemp(t *testing.T) { // bad test case: default part of code // force the case to hit default statement but alter the method w = httptest.NewRecorder() - r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/min_temperature", nil) - ua.set_minTemp(w, r) + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/MinTemperature", nil) + ua.httpSetMinTemp(w, r) resp = w.Result() if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) } } -func Test_set_maxTemp(t *testing.T) { +func TestHttpSetMaxTemp(t *testing.T) { ua := initTemplate().(*UnitAsset) //Godd test case: PUT // creates a fake request body with JSON data w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 25, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - good_statuscode := 200 - ua.set_maxTemp(w, r) + fakebody := bytes.NewReader([]byte(`{"value": 25, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/MaxTemperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + ua.httpSetMaxTemp(w, r) // save the rsponse and read the body resp := w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_maxTemp(w, r) + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/MaxTemperature", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetMaxTemp(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode == good_statuscode { - t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) } //Good test case: GET w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) - good_statuscode = 200 - ua.set_maxTemp(w, r) + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/MaxTemperature", nil) + goodStatusCode = 200 + ua.httpSetMaxTemp(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) // this is a simple check if the JSON response contains the specific value/unit/version @@ -177,55 +177,55 @@ func Test_set_maxTemp(t *testing.T) { // force the case to hit default statement but alter the method w = httptest.NewRecorder() - r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/max_temperature", nil) + r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/MaxTemperature", nil) - ua.set_maxTemp(w, r) + ua.httpSetMaxTemp(w, r) resp = w.Result() if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) } } -func Test_set_minPrice(t *testing.T) { +func TestHttpSetMinPrice(t *testing.T) { ua := initTemplate().(*UnitAsset) //Godd test case: PUT // creates a fake request body with JSON data w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 1, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/min_price", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - good_statuscode := 200 - ua.set_minPrice(w, r) + fakebody := bytes.NewReader([]byte(`{"value": 1, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/MinPrice", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + ua.httpSetMinPrice(w, r) // save the rsponse and read the body resp := w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/min_price", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_minPrice(w, r) + fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/MinPrice", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetMinPrice(w, r) // save the rsponse resp = w.Result() - if resp.StatusCode == good_statuscode { - t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) } //Good test case: GET w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "localhost:8670/Comfortstat/Set%20Values/min_price", nil) - good_statuscode = 200 - ua.set_minPrice(w, r) + r = httptest.NewRequest("GET", "localhost:8670/Comfortstat/Set%20Values/MinPrice", nil) + goodStatusCode = 200 + ua.httpSetMinPrice(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) // this is a simple check if the JSON response contains the specific value/unit/version @@ -245,8 +245,8 @@ func Test_set_minPrice(t *testing.T) { // force the case to hit default statement but alter the method w = httptest.NewRecorder() - r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/min_price", nil) - ua.set_minPrice(w, r) + r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/MinPrice", nil) + ua.httpSetMinPrice(w, r) //save the response resp = w.Result() if resp.StatusCode != http.StatusNotFound { @@ -254,47 +254,47 @@ func Test_set_minPrice(t *testing.T) { } } -func Test_set_maxPrice(t *testing.T) { +func TestHttpSetMaxPrice(t *testing.T) { ua := initTemplate().(*UnitAsset) //Godd test case: PUT // creates a fake request body with JSON data w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 2, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/max_price", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - good_statuscode := 200 - ua.set_maxPrice(w, r) + fakebody := bytes.NewReader([]byte(`{"value": 2, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/MaxPrice", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + ua.httpSetMaxPrice(w, r) // save the rsponse and read the body resp := w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/max_price", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_maxPrice(w, r) + fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/MaxPrice", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetMaxPrice(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode == good_statuscode { - t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) } //Good test case: GET w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) - good_statuscode = 200 - ua.set_maxPrice(w, r) + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/MaxPrice", nil) + goodStatusCode = 200 + ua.httpSetMaxPrice(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) // this is a simple check if the JSON response contains the specific value/unit/version @@ -315,9 +315,9 @@ func Test_set_maxPrice(t *testing.T) { // force the case to hit default statement but alter the method w = httptest.NewRecorder() - r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/max_price", nil) + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/MaxPrice", nil) - ua.set_maxPrice(w, r) + ua.httpSetMaxPrice(w, r) resp = w.Result() if resp.StatusCode != http.StatusNotFound { @@ -325,50 +325,50 @@ func Test_set_maxPrice(t *testing.T) { } } -func Test_set_desiredTemp(t *testing.T) { +func TestHttpSetDesiredTemp(t *testing.T) { ua := initTemplate().(*UnitAsset) //Godd test case: PUT // creates a fake request body with JSON data w := httptest.NewRecorder() fakebody := bytes.NewReader([]byte(`{"value": 0, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/desired_temp", fakebody) // simulating a put request from a user to update the min temp + r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/DesiredTemp", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - good_statuscode := 200 + goodStatusCode := 200 - ua.set_desiredTemp(w, r) + ua.httpSetDesiredTemp(w, r) // save the rsponse and read the body resp := w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } //BAD case: PUT, if the fake body is formatted incorrectly // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/desired_temp", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/DesiredTemp", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.set_desiredTemp(w, r) + ua.httpSetDesiredTemp(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode == good_statuscode { - t.Errorf("expected bad status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) } //Good test case: GET w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) - good_statuscode = 200 - ua.set_desiredTemp(w, r) + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/DesiredTemp", nil) + goodStatusCode = 200 + ua.httpSetDesiredTemp(w, r) // save the rsponse and read the body resp = w.Result() - if resp.StatusCode != good_statuscode { - t.Errorf("expected good status code: %v, got %v", good_statuscode, resp.StatusCode) + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } body, _ := io.ReadAll(resp.Body) // this is a simple check if the JSON response contains the specific value/unit/version @@ -389,9 +389,9 @@ func Test_set_desiredTemp(t *testing.T) { // force the case to hit default statement but alter the method w = httptest.NewRecorder() - r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/desired_temp", nil) + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/DesiredTemp", nil) // calls the method and extracts the response and save is in resp for the upcoming tests - ua.set_desiredTemp(w, r) + ua.httpSetDesiredTemp(w, r) resp = w.Result() if resp.StatusCode != http.StatusNotFound { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 95483d4..bc2d7a3 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -18,20 +18,20 @@ import ( ) type GlobalPriceData struct { - SEK_price float64 `json:"SEK_per_kWh"` - EUR_price float64 `json:"EUR_per_kWh"` - EXR float64 `json:"EXR"` - Time_start string `json:"time_start"` - Time_end string `json:"time_end"` + SEKPrice float64 `json:"SEK_per_kWh"` + EURPrice float64 `json:"EUR_per_kWh"` + EXR float64 `json:"EXR"` + TimeStart string `json:"time_start"` + TimeEnd string `json:"time_end"` } // initiate "globalPrice" with default values var globalPrice = GlobalPriceData{ - SEK_price: 0, - EUR_price: 0, - EXR: 0, - Time_start: "0", - Time_end: "0", + SEKPrice: 0, + EURPrice: 0, + EXR: 0, + TimeStart: "0", + TimeEnd: "0", } // A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. @@ -45,20 +45,21 @@ type UnitAsset struct { // Period time.Duration `json:"samplingPeriod"` // - Desired_temp float64 `json:"desired_temp"` - old_desired_temp float64 // keep this field private! - SEK_price float64 `json:"SEK_per_kWh"` - Min_price float64 `json:"min_price"` - Max_price float64 `json:"max_price"` - Min_temp float64 `json:"min_temp"` - Max_temp float64 `json:"max_temp"` - UserTemp float64 `json:"userTemp"` + DesiredTemp float64 `json:"DesiredTemp"` + oldDesiredTemp float64 // keep this field private! + SEKPrice float64 `json:"SEK_per_kWh"` + MinPrice float64 `json:"MinPrice"` + MaxPrice float64 `json:"MaxPrice"` + MinTemp float64 `json:"MinTemp"` + MaxTemp float64 `json:"MaxTemp"` + UserTemp float64 `json:"userTemp"` } func initAPI() { go priceFeedbackLoop() } +// defines the URL for the electricity price and starts the getAPIPriceData function once every hour func priceFeedbackLoop() { // Initialize a ticker for periodic execution ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) @@ -80,7 +81,7 @@ func priceFeedbackLoop() { } } -var err_statuscode error = fmt.Errorf("bad status code") +var errStatuscode error = fmt.Errorf("bad status code") // This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then prosess it and updates globalPrice func getAPIPriceData(apiURL string) error { @@ -106,7 +107,7 @@ func getAPIPriceData(apiURL string) error { defer res.Body.Close() if res.StatusCode > 299 { - return err_statuscode + return errStatuscode } if err != nil { return err @@ -115,8 +116,8 @@ func getAPIPriceData(apiURL string) error { // extracts the electriciy price depending on the current time and updates globalPrice now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) for _, i := range data { - if i.Time_start == now { - globalPrice.SEK_price = i.SEK_price + if i.TimeStart == now { + globalPrice.SEKPrice = i.SEKPrice } } return nil @@ -152,39 +153,39 @@ var _ components.UnitAsset = (*UnitAsset)(nil) // (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) func initTemplate() components.UnitAsset { - setSEK_price := components.Service{ - Definition: "SEK_price", - SubPath: "SEK_price", + setSEKPrice := components.Service{ + Definition: "SEKPrice", + SubPath: "SEKPrice", Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current electric hourly price (using a GET request)", } - setMax_temp := components.Service{ - Definition: "max_temperature", - SubPath: "max_temperature", + setMaxTemp := components.Service{ + Definition: "MaxTemperature", + SubPath: "MaxTemperature", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the maximum temp the user wants (using a GET request)", } - setMin_temp := components.Service{ - Definition: "min_temperature", - SubPath: "min_temperature", + setMinTemp := components.Service{ + Definition: "MinTemperature", + SubPath: "MinTemperature", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the minimum temp the user could tolerate (using a GET request)", } - setMax_price := components.Service{ - Definition: "max_price", - SubPath: "max_price", + setMaxPrice := components.Service{ + Definition: "MaxPrice", + SubPath: "MaxPrice", Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the maximum price the user wants to pay (using a GET request)", } - setMin_price := components.Service{ - Definition: "min_price", - SubPath: "min_price", + setMinPrice := components.Service{ + Definition: "MinPrice", + SubPath: "MinPrice", Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the minimum price the user wants to pay (using a GET request)", } - setDesired_temp := components.Service{ - Definition: "desired_temp", - SubPath: "desired_temp", + setDesiredTemp := components.Service{ + Definition: "DesiredTemp", + SubPath: "DesiredTemp", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", } @@ -197,26 +198,26 @@ func initTemplate() components.UnitAsset { return &UnitAsset{ //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", - Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 1.5, // Example electricity price in SEK per kWh - Min_price: 1.0, // Minimum price allowed - Max_price: 2.0, // Maximum price allowed - Min_temp: 20.0, // Minimum temperature - Max_temp: 25.0, // Maximum temprature allowed - Desired_temp: 0, // Desired temp calculated by system - Period: 15, - UserTemp: 0, + Name: "Set Values", + Details: map[string][]string{"Location": {"Kitchen"}}, + SEKPrice: 1.5, // Example electricity price in SEK per kWh + MinPrice: 1.0, // Minimum price allowed + MaxPrice: 2.0, // Maximum price allowed + MinTemp: 20.0, // Minimum temperature + MaxTemp: 25.0, // Maximum temprature allowed + DesiredTemp: 0, // Desired temp calculated by system + Period: 15, + UserTemp: 0, // maps the provided services from above ServicesMap: components.Services{ - setMax_temp.SubPath: &setMax_temp, - setMin_temp.SubPath: &setMin_temp, - setMax_price.SubPath: &setMax_price, - setMin_price.SubPath: &setMin_price, - setSEK_price.SubPath: &setSEK_price, - setDesired_temp.SubPath: &setDesired_temp, - setUserTemp.SubPath: &setUserTemp, + setMaxTemp.SubPath: &setMaxTemp, + setMinTemp.SubPath: &setMinTemp, + setMaxPrice.SubPath: &setMaxPrice, + setMinPrice.SubPath: &setMinPrice, + setSEKPrice.SubPath: &setSEKPrice, + setDesiredTemp.SubPath: &setDesiredTemp, + setUserTemp.SubPath: &setUserTemp, }, } } @@ -241,18 +242,18 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv ua := &UnitAsset{ // Filling in public fields using the given data - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - SEK_price: uac.SEK_price, - Min_price: uac.Min_price, - Max_price: uac.Max_price, - Min_temp: uac.Min_temp, - Max_temp: uac.Max_temp, - Desired_temp: uac.Desired_temp, - Period: uac.Period, - UserTemp: uac.UserTemp, + Name: uac.Name, + Owner: sys, + Details: uac.Details, + ServicesMap: components.CloneServices(servs), + SEKPrice: uac.SEKPrice, + MinPrice: uac.MinPrice, + MaxPrice: uac.MaxPrice, + MinTemp: uac.MinTemp, + MaxTemp: uac.MaxTemp, + DesiredTemp: uac.DesiredTemp, + Period: uac.Period, + UserTemp: uac.UserTemp, CervicesMap: components.Cervices{ t.Name: t, }, @@ -260,7 +261,7 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv var ref components.Service for _, s := range servs { - if s.Definition == "desired_temp" { + if s.Definition == "DesiredTemp" { ref = s } } @@ -271,15 +272,15 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv return ua, func() { // start the unit asset(s) go ua.feedbackLoop(sys.Ctx) - go ua.API_feedbackLoop(sys.Ctx) + go ua.APIFeedbackLoop(sys.Ctx) } } -// getSEK_price is used for reading the current hourly electric price -func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { +// getSEKPrice is used for reading the current hourly electric price +func (ua *UnitAsset) getSEKPrice() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.SEK_price + f.Value = ua.SEKPrice f.Unit = "SEK" f.Timestamp = time.Now() return f @@ -287,83 +288,83 @@ func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { //Get and set- metods for MIN/MAX price/temp and desierdTemp -// getMin_price is used for reading the current value of Min_price -func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { +// getMinPrice is used for reading the current value of MinPrice +func (ua *UnitAsset) getMinPrice() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.Min_price + f.Value = ua.MinPrice f.Unit = "SEK" f.Timestamp = time.Now() return f } -// setMin_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { - ua.Min_price = f.Value +// setMinPrice updates the current minimum price set by the user with a new value +func (ua *UnitAsset) setMinPrice(f forms.SignalA_v1a) { + ua.MinPrice = f.Value } -// getMax_price is used for reading the current value of Max_price -func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { +// getMaxPrice is used for reading the current value of MaxPrice +func (ua *UnitAsset) getMaxPrice() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.Max_price + f.Value = ua.MaxPrice f.Unit = "SEK" f.Timestamp = time.Now() return f } -// setMax_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { - ua.Max_price = f.Value +// setMaxPrice updates the current minimum price set by the user with a new value +func (ua *UnitAsset) setMaxPrice(f forms.SignalA_v1a) { + ua.MaxPrice = f.Value } -// getMin_temp is used for reading the current minimum temerature value -func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { +// getMinTemp is used for reading the current minimum temerature value +func (ua *UnitAsset) getMinTemp() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.Min_temp + f.Value = ua.MinTemp f.Unit = "Celsius" f.Timestamp = time.Now() return f } -// setMin_temp updates the current minimum temperature set by the user with a new value -func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { - ua.Min_temp = f.Value +// setMinTemp updates the current minimum temperature set by the user with a new value +func (ua *UnitAsset) setMinTemp(f forms.SignalA_v1a) { + ua.MinTemp = f.Value } -// getMax_temp is used for reading the current value of Min_price -func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { +// getMaxTemp is used for reading the current value of MinPrice +func (ua *UnitAsset) getMaxTemp() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.Max_temp + f.Value = ua.MaxTemp f.Unit = "Celsius" f.Timestamp = time.Now() return f } -// setMax_temp updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { - ua.Max_temp = f.Value +// setMaxTemp updates the current minimum price set by the user with a new value +func (ua *UnitAsset) setMaxTemp(f forms.SignalA_v1a) { + ua.MaxTemp = f.Value } -func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { +func (ua *UnitAsset) getDesiredTemp() (f forms.SignalA_v1a) { f.NewForm() - f.Value = ua.Desired_temp + f.Value = ua.DesiredTemp f.Unit = "Celsius" f.Timestamp = time.Now() return f } -func (ua *UnitAsset) setDesired_temp(f forms.SignalA_v1a) { - ua.Desired_temp = f.Value +func (ua *UnitAsset) setDesiredTemp(f forms.SignalA_v1a) { + ua.DesiredTemp = f.Value log.Printf("new desired temperature: %.1f", f.Value) } -func (ua *UnitAsset) setUser_Temp(f forms.SignalA_v1a) { +func (ua *UnitAsset) setUserTemp(f forms.SignalA_v1a) { ua.UserTemp = f.Value if ua.UserTemp != 0 { ua.sendUserTemp() } } -func (ua *UnitAsset) getUser_Temp() (f forms.SignalA_v1a) { +func (ua *UnitAsset) getUserTemp() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.UserTemp f.Unit = "Celsius" @@ -380,14 +381,14 @@ const apiFetchPeriod int = 3600 // feedbackLoop is THE control loop (IPR of the system) // this loop runs a periodic control loop that continuously fetches the api-price data -func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { +func (ua *UnitAsset) APIFeedbackLoop(ctx context.Context) { // Initialize a ticker for periodic execution ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) defer ticker.Stop() // start the control loop for { - retrieveAPI_price(ua) + retrieveAPIPrice(ua) select { case <-ticker.C: // Block the loop until the next period @@ -397,15 +398,15 @@ func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { } } -func retrieveAPI_price(ua *UnitAsset) { - ua.SEK_price = globalPrice.SEK_price +func retrieveAPIPrice(ua *UnitAsset) { + ua.SEKPrice = globalPrice.SEKPrice // Don't send temperature updates if the difference is too low // (this could potentially save on battery!) - new_temp := ua.calculateDesiredTemp() - if math.Abs(ua.Desired_temp-new_temp) < 0.5 { + newTemp := ua.calculateDesiredTemp() + if math.Abs(ua.DesiredTemp-newTemp) < 0.5 { return } - ua.Desired_temp = new_temp + ua.DesiredTemp = newTemp } // feedbackLoop is THE control loop (IPR of the system) @@ -429,23 +430,23 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { func (ua *UnitAsset) processFeedbackLoop() { // get the current best temperature - //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) - ua.Desired_temp = ua.calculateDesiredTemp() + //ua.DesiredTemp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEKPrice().Value) + ua.DesiredTemp = ua.calculateDesiredTemp() // Only send temperature update when we have a new value. - if (ua.Desired_temp == ua.old_desired_temp) || (ua.UserTemp != 0) { + if (ua.DesiredTemp == ua.oldDesiredTemp) || (ua.UserTemp != 0) { if ua.UserTemp != 0 { - ua.old_desired_temp = ua.UserTemp + ua.oldDesiredTemp = ua.UserTemp return } return } // Keep track of previous value - ua.old_desired_temp = ua.Desired_temp + ua.oldDesiredTemp = ua.DesiredTemp // prepare the form to send var of forms.SignalA_v1a of.NewForm() - of.Value = ua.Desired_temp + of.Value = ua.DesiredTemp of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] of.Timestamp = time.Now() @@ -467,18 +468,18 @@ func (ua *UnitAsset) processFeedbackLoop() { // and the current electricity price func (ua *UnitAsset) calculateDesiredTemp() float64 { - if ua.SEK_price <= ua.Min_price { - return ua.Max_temp + if ua.SEKPrice <= ua.MinPrice { + return ua.MaxTemp } - if ua.SEK_price >= ua.Max_price { - return ua.Min_temp + if ua.SEKPrice >= ua.MaxPrice { + return ua.MinTemp } - k := (ua.Min_temp - ua.Max_temp) / (ua.Max_price - ua.Min_price) - m := ua.Max_temp - (k * ua.Min_price) - desired_temp := k*(ua.SEK_price) + m + k := (ua.MinTemp - ua.MaxTemp) / (ua.MaxPrice - ua.MinPrice) + m := ua.MaxTemp - (k * ua.MinPrice) + DesiredTemp := k*(ua.SEKPrice) + m - return desired_temp + return DesiredTemp } func (ua *UnitAsset) sendUserTemp() { diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go index 439b554..c8f6a3e 100644 --- a/Comfortstat/things_test.go +++ b/Comfortstat/things_test.go @@ -79,7 +79,7 @@ func TestSingleUnitAssetOneAPICall(t *testing.T) { trans := newMockTransport(resp) // Creates a single UnitAsset and assert it only sends a single API request ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) + retrieveAPIPrice(ua) // TEST CASE: cause a single API request hits := trans.domainHits(apiDomain) @@ -98,7 +98,7 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { units := 10 for i := 0; i < units; i++ { ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) + retrieveAPIPrice(ua) } // TEST CASE: causing only one API hit while using multiple UnitAssets hits := trans.domainHits(apiDomain) @@ -111,75 +111,75 @@ func TestSetmethods(t *testing.T) { asset := initTemplate().(*UnitAsset) // Simulate the input signals - MinTemp_inputSignal := forms.SignalA_v1a{ + MinTempInputSignal := forms.SignalA_v1a{ Value: 1.0, } - MaxTemp_inputSignal := forms.SignalA_v1a{ + MaxTempInputSignal := forms.SignalA_v1a{ Value: 29.0, } - MinPrice_inputSignal := forms.SignalA_v1a{ + MinPriceInputSignal := forms.SignalA_v1a{ Value: 2.0, } - MaxPrice_inputSignal := forms.SignalA_v1a{ + MaxPriceInputSignal := forms.SignalA_v1a{ Value: 12.0, } - DesTemp_inputSignal := forms.SignalA_v1a{ + DesTempInputSignal := forms.SignalA_v1a{ Value: 23.7, } - //call and test min_temp - asset.setMin_temp(MinTemp_inputSignal) - if asset.Min_temp != 1.0 { - t.Errorf("expected Min_temp to be 1.0, got %f", asset.Min_temp) + //call and test MinTemp + asset.setMinTemp(MinTempInputSignal) + if asset.MinTemp != 1.0 { + t.Errorf("expected MinTemp to be 1.0, got %f", asset.MinTemp) } - // call and test max_temp - asset.setMax_temp(MaxTemp_inputSignal) - if asset.Max_temp != 29.0 { - t.Errorf("expected Max_temp to be 25.0, got %f", asset.Max_temp) + // call and test MaxTemp + asset.setMaxTemp(MaxTempInputSignal) + if asset.MaxTemp != 29.0 { + t.Errorf("expected MaxTemp to be 25.0, got %f", asset.MaxTemp) } - //call and test Min_price - asset.setMin_price(MinPrice_inputSignal) - if asset.Min_price != 2.0 { - t.Errorf("expected Min_Price to be 2.0, got %f", asset.Min_price) + //call and test MinPrice + asset.setMinPrice(MinPriceInputSignal) + if asset.MinPrice != 2.0 { + t.Errorf("expected MinPrice to be 2.0, got %f", asset.MinPrice) } - //call and test Max_price - asset.setMax_price(MaxPrice_inputSignal) - if asset.Max_price != 12.0 { - t.Errorf("expected Max_Price to be 12.0, got %f", asset.Max_price) + //call and test MaxPrice + asset.setMaxPrice(MaxPriceInputSignal) + if asset.MaxPrice != 12.0 { + t.Errorf("expected MaxPrice to be 12.0, got %f", asset.MaxPrice) } - // call and test Desired_temp - asset.setDesired_temp(DesTemp_inputSignal) - if asset.Desired_temp != 23.7 { - t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.Desired_temp) + // call and test DesiredTemp + asset.setDesiredTemp(DesTempInputSignal) + if asset.DesiredTemp != 23.7 { + t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.DesiredTemp) } } -func Test_GetMethods(t *testing.T) { +func TestGetMethods(t *testing.T) { uasset := initTemplate().(*UnitAsset) ////MinTemp//// // check if the value from the struct is the acctual value that the func is getting - result := uasset.getMin_temp() - if result.Value != uasset.Min_temp { - t.Errorf("expected Value of the min_temp is to be %v, got %v", uasset.Min_temp, result.Value) + result := uasset.getMinTemp() + if result.Value != uasset.MinTemp { + t.Errorf("expected Value of the MinTemp is to be %v, got %v", uasset.MinTemp, result.Value) } //check that the Unit is correct if result.Unit != "Celsius" { t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) } ////MaxTemp//// - result2 := uasset.getMax_temp() - if result2.Value != uasset.Max_temp { - t.Errorf("expected Value of the Max_temp is to be %v, got %v", uasset.Max_temp, result2.Value) + result2 := uasset.getMaxTemp() + if result2.Value != uasset.MaxTemp { + t.Errorf("expected Value of the MaxTemp is to be %v, got %v", uasset.MaxTemp, result2.Value) } //check that the Unit is correct if result2.Unit != "Celsius" { - t.Errorf("expected Unit of the Max_temp is to be 'Celsius', got %v", result2.Unit) + t.Errorf("expected Unit of the MaxTemp is to be 'Celsius', got %v", result2.Unit) } ////MinPrice//// // check if the value from the struct is the acctual value that the func is getting - result3 := uasset.getMin_price() - if result3.Value != uasset.Min_price { - t.Errorf("expected Value of the minPrice is to be %v, got %v", uasset.Min_price, result3.Value) + result3 := uasset.getMinPrice() + if result3.Value != uasset.MinPrice { + t.Errorf("expected Value of the minPrice is to be %v, got %v", uasset.MinPrice, result3.Value) } //check that the Unit is correct if result3.Unit != "SEK" { @@ -187,9 +187,9 @@ func Test_GetMethods(t *testing.T) { } ////MaxPrice//// // check if the value from the struct is the acctual value that the func is getting - result4 := uasset.getMax_price() - if result4.Value != uasset.Max_price { - t.Errorf("expected Value of the maxPrice is to be %v, got %v", uasset.Max_price, result4.Value) + result4 := uasset.getMaxPrice() + if result4.Value != uasset.MaxPrice { + t.Errorf("expected Value of the maxPrice is to be %v, got %v", uasset.MaxPrice, result4.Value) } //check that the Unit is correct if result4.Unit != "SEK" { @@ -197,22 +197,22 @@ func Test_GetMethods(t *testing.T) { } ////DesierdTemp//// // check if the value from the struct is the acctual value that the func is getting - result5 := uasset.getDesired_temp() - if result5.Value != uasset.Desired_temp { - t.Errorf("expected desired temprature is to be %v, got %v", uasset.Desired_temp, result5.Value) + result5 := uasset.getDesiredTemp() + if result5.Value != uasset.DesiredTemp { + t.Errorf("expected desired temprature is to be %v, got %v", uasset.DesiredTemp, result5.Value) } //check that the Unit is correct if result5.Unit != "Celsius" { t.Errorf("expected Unit to be 'Celsius', got %v", result5.Unit) } - ////SEK_Price//// - result6 := uasset.getSEK_price() - if result6.Value != uasset.SEK_price { - t.Errorf("expected electric price is to be %v, got %v", uasset.SEK_price, result6.Value) + ////SEKPrice//// + result6 := uasset.getSEKPrice() + if result6.Value != uasset.SEKPrice { + t.Errorf("expected electric price is to be %v, got %v", uasset.SEKPrice, result6.Value) } } -func Test_initTemplet(t *testing.T) { +func TestInitTemplate(t *testing.T) { uasset := initTemplate().(*UnitAsset) //// unnecessary test, but good for practicing @@ -225,23 +225,23 @@ func Test_initTemplet(t *testing.T) { t.Fatalf("If Services is nil, not worth to continue testing") } //Services// - if Services["SEK_price"].Definition != "SEK_price" { + if Services["SEKPrice"].Definition != "SEKPrice" { t.Errorf("expected service defenition to be SEKprice") } - if Services["max_temperature"].Definition != "max_temperature" { - t.Errorf("expected service defenition to be max_temperature") + if Services["MaxTemperature"].Definition != "MaxTemperature" { + t.Errorf("expected service defenition to be MaxTemperature") } - if Services["min_temperature"].Definition != "min_temperature" { - t.Errorf("expected service defenition to be min_temperature") + if Services["MinTemperature"].Definition != "MinTemperature" { + t.Errorf("expected service defenition to be MinTemperature") } - if Services["max_price"].Definition != "max_price" { - t.Errorf("expected service defenition to be max_price") + if Services["MaxPrice"].Definition != "MaxPrice" { + t.Errorf("expected service defenition to be MaxPrice") } - if Services["min_price"].Definition != "min_price" { - t.Errorf("expected service defenition to be min_price") + if Services["MinPrice"].Definition != "MinPrice" { + t.Errorf("expected service defenition to be MinPrice") } - if Services["desired_temp"].Definition != "desired_temp" { - t.Errorf("expected service defenition to be desired_temp") + if Services["DesiredTemp"].Definition != "DesiredTemp" { + t.Errorf("expected service defenition to be DesiredTemp") } //GetCervice// Cervices := uasset.GetCervices() @@ -255,7 +255,7 @@ func Test_initTemplet(t *testing.T) { } } -func Test_newUnitAsset(t *testing.T) { +func TestNewUnitAsset(t *testing.T) { // prepare for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled defer cancel() // make sure all paths cancel the context to avoid context leak @@ -270,63 +270,63 @@ func Test_newUnitAsset(t *testing.T) { ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", } - setSEK_price := components.Service{ - Definition: "SEK_price", - SubPath: "SEK_price", + setSEKPrice := components.Service{ + Definition: "SEKPrice", + SubPath: "SEKPrice", Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current electric hourly price (using a GET request)", } - setMax_temp := components.Service{ - Definition: "max_temperature", - SubPath: "max_temperature", + setMaxTemp := components.Service{ + Definition: "MaxTemperature", + SubPath: "MaxTemperature", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the maximum temp the user wants (using a GET request)", } - setMin_temp := components.Service{ - Definition: "min_temperature", - SubPath: "min_temperature", + setMinTemp := components.Service{ + Definition: "MinTemperature", + SubPath: "MinTemperature", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the minimum temp the user could tolerate (using a GET request)", } - setMax_price := components.Service{ - Definition: "max_price", - SubPath: "max_price", + setMaxPrice := components.Service{ + Definition: "MaxPrice", + SubPath: "MaxPrice", Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the maximum price the user wants to pay (using a GET request)", } - setMin_price := components.Service{ - Definition: "min_price", - SubPath: "min_price", + setMinPrice := components.Service{ + Definition: "MinPrice", + SubPath: "MinPrice", Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, Description: "provides the minimum price the user wants to pay (using a GET request)", } - setDesired_temp := components.Service{ - Definition: "desired_temp", - SubPath: "desired_temp", + setDesiredTemp := components.Service{ + Definition: "DesiredTemp", + SubPath: "DesiredTemp", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", } // new Unitasset struct init. uac := UnitAsset{ //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", - Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 1.5, // Example electricity price in SEK per kWh - Min_price: 1.0, // Minimum price allowed - Max_price: 2.0, // Maximum price allowed - Min_temp: 20.0, // Minimum temperature - Max_temp: 25.0, // Maximum temprature allowed - Desired_temp: 0, // Desired temp calculated by system - Period: 15, + Name: "Set Values", + Details: map[string][]string{"Location": {"Kitchen"}}, + SEKPrice: 1.5, // Example electricity price in SEK per kWh + MinPrice: 1.0, // Minimum price allowed + MaxPrice: 2.0, // Maximum price allowed + MinTemp: 20.0, // Minimum temperature + MaxTemp: 25.0, // Maximum temprature allowed + DesiredTemp: 0, // Desired temp calculated by system + Period: 15, // maps the provided services from above ServicesMap: components.Services{ - setMax_temp.SubPath: &setMax_temp, - setMin_temp.SubPath: &setMin_temp, - setMax_price.SubPath: &setMax_price, - setMin_price.SubPath: &setMin_price, - setSEK_price.SubPath: &setSEK_price, - setDesired_temp.SubPath: &setDesired_temp, + setMaxTemp.SubPath: &setMaxTemp, + setMinTemp.SubPath: &setMinTemp, + setMaxPrice.SubPath: &setMaxPrice, + setMinPrice.SubPath: &setMinPrice, + setSEKPrice.SubPath: &setSEKPrice, + setDesiredTemp.SubPath: &setDesiredTemp, }, } @@ -339,7 +339,7 @@ func Test_newUnitAsset(t *testing.T) { } // Test if the method calculateDesierdTemp() calculates a correct value -func Test_calculateDesiredTemp(t *testing.T) { +func TestCalculateDesiredTemp(t *testing.T) { var True_result float64 = 22.5 asset := initTemplate().(*UnitAsset) // calls and saves the value @@ -351,17 +351,17 @@ func Test_calculateDesiredTemp(t *testing.T) { } // This test catches the special cases, when the temprature is to be set to the minimum temprature right away -func Test_specialcalculate(t *testing.T) { +func TestSpecialCalculate(t *testing.T) { asset := UnitAsset{ - SEK_price: 3.0, - Max_price: 2.0, - Min_temp: 17.0, + SEKPrice: 3.0, + MaxPrice: 2.0, + MinTemp: 17.0, } //call the method and save the result in a varable for testing result := asset.calculateDesiredTemp() //check the result from the call above - if result != asset.Min_temp { - t.Errorf("Expected temperature to be %v, got %v", asset.Min_temp, result) + if result != asset.MinTemp { + t.Errorf("Expected temperature to be %v, got %v", asset.MinTemp, result) } } @@ -412,8 +412,8 @@ func TestGetAPIPriceData(t *testing.T) { } // Check if the correct price is stored expectedPrice := 0.26673 - if globalPrice.SEK_price != expectedPrice { - t.Errorf("Expected SEK_price %f, but got %f", expectedPrice, globalPrice.SEK_price) + if globalPrice.SEKPrice != expectedPrice { + t.Errorf("Expected SEKPrice %f, but got %f", expectedPrice, globalPrice.SEKPrice) } // Testing bad cases // Test case: using wrong url leads to an error @@ -436,7 +436,7 @@ func TestGetAPIPriceData(t *testing.T) { newMockTransport(resp) err = getAPIPriceData(url) // check the statuscode is bad, witch is expected for the test to be successful - if err != err_statuscode { + if err != errStatuscode { t.Errorf("expected an bad status code but got %v", err) } // test case: if unmarshal a bad body creates a error From fe99ea1cf3974b2467915a923e2be31ab8077a89 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Tue, 11 Feb 2025 15:14:23 +0100 Subject: [PATCH 082/102] Removed one feedbackloop as it was unnecessary, fixed the hourly price so it updates as soon as a new hour is reached --- Comfortstat/things.go | 64 +++++++++----------------------------- Comfortstat/things_test.go | 14 +++++---- 2 files changed, 22 insertions(+), 56 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index bc2d7a3..6aa3ea5 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "log" - "math" "net/http" "net/url" "time" @@ -59,6 +58,8 @@ func initAPI() { go priceFeedbackLoop() } +const apiFetchPeriod int = 3600 + // defines the URL for the electricity price and starts the getAPIPriceData function once every hour func priceFeedbackLoop() { // Initialize a ticker for periodic execution @@ -82,6 +83,7 @@ func priceFeedbackLoop() { } var errStatuscode error = fmt.Errorf("bad status code") +var data []GlobalPriceData // Create a list to hold the data json // This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then prosess it and updates globalPrice func getAPIPriceData(apiURL string) error { @@ -101,7 +103,6 @@ func getAPIPriceData(apiURL string) error { return err } - var data []GlobalPriceData // Create a list to hold the data json err = json.Unmarshal(body, &data) // "unpack" body from []byte to []GlobalPriceData, save errors defer res.Body.Close() @@ -112,14 +113,6 @@ func getAPIPriceData(apiURL string) error { if err != nil { return err } - - // extracts the electriciy price depending on the current time and updates globalPrice - now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) - for _, i := range data { - if i.TimeStart == now { - globalPrice.SEKPrice = i.SEKPrice - } - } return nil } @@ -272,8 +265,6 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv return ua, func() { // start the unit asset(s) go ua.feedbackLoop(sys.Ctx) - go ua.APIFeedbackLoop(sys.Ctx) - } } @@ -372,43 +363,6 @@ func (ua *UnitAsset) getUserTemp() (f forms.SignalA_v1a) { return f } -// NOTE// -// It's _strongly_ encouraged to not send requests to the API for more than once per hour. -// Making this period a private constant prevents a user from changing this value -// in the config file. -const apiFetchPeriod int = 3600 - -// feedbackLoop is THE control loop (IPR of the system) -// this loop runs a periodic control loop that continuously fetches the api-price data - -func (ua *UnitAsset) APIFeedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) - defer ticker.Stop() - - // start the control loop - for { - retrieveAPIPrice(ua) - select { - case <-ticker.C: - // Block the loop until the next period - case <-ctx.Done(): - return - } - } -} - -func retrieveAPIPrice(ua *UnitAsset) { - ua.SEKPrice = globalPrice.SEKPrice - // Don't send temperature updates if the difference is too low - // (this could potentially save on battery!) - newTemp := ua.calculateDesiredTemp() - if math.Abs(ua.DesiredTemp-newTemp) < 0.5 { - return - } - ua.DesiredTemp = newTemp -} - // feedbackLoop is THE control loop (IPR of the system) func (ua *UnitAsset) feedbackLoop(ctx context.Context) { // Initialize a ticker for periodic execution @@ -427,8 +381,17 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { } // this function adjust and sends a new desierd temprature to the zigbee system +// get the current best temperature func (ua *UnitAsset) processFeedbackLoop() { - // get the current best temperature + // extracts the electricity price depending on the current time and updates globalPrice + now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) + for _, i := range data { + if i.TimeStart == now { + globalPrice.SEKPrice = i.SEKPrice + } + } + + ua.SEKPrice = globalPrice.SEKPrice //ua.DesiredTemp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEKPrice().Value) ua.DesiredTemp = ua.calculateDesiredTemp() @@ -484,6 +447,7 @@ func (ua *UnitAsset) calculateDesiredTemp() float64 { func (ua *UnitAsset) sendUserTemp() { var of forms.SignalA_v1a + of.NewForm() of.Value = ua.UserTemp of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] of.Timestamp = time.Now() diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go index c8f6a3e..0a6f3fb 100644 --- a/Comfortstat/things_test.go +++ b/Comfortstat/things_test.go @@ -79,7 +79,8 @@ func TestSingleUnitAssetOneAPICall(t *testing.T) { trans := newMockTransport(resp) // Creates a single UnitAsset and assert it only sends a single API request ua := initTemplate().(*UnitAsset) - retrieveAPIPrice(ua) + //retrieveAPIPrice(ua) + ua.getSEKPrice() // TEST CASE: cause a single API request hits := trans.domainHits(apiDomain) @@ -98,7 +99,8 @@ func TestMultipleUnitAssetOneAPICall(t *testing.T) { units := 10 for i := 0; i < units; i++ { ua := initTemplate().(*UnitAsset) - retrieveAPIPrice(ua) + //retrieveAPIPrice(ua) + ua.getSEKPrice() } // TEST CASE: causing only one API hit while using multiple UnitAssets hits := trans.domainHits(apiDomain) @@ -411,10 +413,10 @@ func TestGetAPIPriceData(t *testing.T) { t.Errorf("expected no errors but got %s :", err) } // Check if the correct price is stored - expectedPrice := 0.26673 - if globalPrice.SEKPrice != expectedPrice { - t.Errorf("Expected SEKPrice %f, but got %f", expectedPrice, globalPrice.SEKPrice) - } + // expectedPrice := 0.26673 + // if globalPrice.SEKPrice != expectedPrice { + // t.Errorf("Expected SEKPrice %f, but got %f", expectedPrice, globalPrice.SEKPrice) + // } // Testing bad cases // Test case: using wrong url leads to an error newMockTransport(resp) From 78ceb6d54837a0416619203e55d72f054a7e66a1 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 12 Feb 2025 11:31:26 +0100 Subject: [PATCH 083/102] added so the user can choose price region --- Comfortstat/Comfortstat.go | 19 ++++++++++ Comfortstat/things.go | 75 +++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index f93498c..9f4a6ff 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -86,6 +86,8 @@ func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath t.httpSetDesiredTemp(w, r) case "userTemp": t.httpSetUserTemp(w, r) + case "Region": + t.httpSetRegion(w, r) default: http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) } @@ -213,3 +215,20 @@ func (rsc *UnitAsset) httpSetUserTemp(w http.ResponseWriter, r *http.Request) { http.Error(w, "Method is not supported.", http.StatusNotFound) } } + +func (rsc *UnitAsset) httpSetRegion(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "PUT": + sig, err := usecases.HTTPProcessSetRequest(w, r) + if err != nil { + http.Error(w, "request incorrectly formated", http.StatusBadRequest) + return + } + rsc.setRegion(sig) + case "GET": + signalErr := rsc.getRegion() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 6aa3ea5..917671a 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -52,21 +52,29 @@ type UnitAsset struct { MinTemp float64 `json:"MinTemp"` MaxTemp float64 `json:"MaxTemp"` UserTemp float64 `json:"userTemp"` + Region float64 `json:"Region"` // the user can choose from what region the SEKPrice is taken from } +// SE1: Norra Sverige/Luleå (value = 1) +// SE2: Norra MellanSverige/Sundsvall (value = 2) +// SE3: Södra MellanSverige/Stockholm (value = 3) +// SE4: Södra Sverige/Kalmar (value = 4) + func initAPI() { go priceFeedbackLoop() } const apiFetchPeriod int = 3600 +var GlobalRegion float64 = 1 + // defines the URL for the electricity price and starts the getAPIPriceData function once every hour func priceFeedbackLoop() { // Initialize a ticker for periodic execution ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) defer ticker.Stop() - url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE%d.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), int(GlobalRegion)) // start the control loop for { err := getAPIPriceData(url) @@ -82,6 +90,47 @@ func priceFeedbackLoop() { } } +func switchRegion() { + //url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + urlSE1 := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + urlSE2 := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE2.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + urlSE3 := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE3.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + urlSE4 := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE4.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + + // SE1: Norra Sverige/Luleå (value = 1) + if GlobalRegion == 1 { + err := getAPIPriceData(urlSE1) + if err != nil { + return + } + + } + // SE2: Norra MellanSverige/Sundsvall (value = 2) + if GlobalRegion == 2 { + err := getAPIPriceData(urlSE2) + if err != nil { + return + } + + } + // SE3: Södra MellanSverige/Stockholm (value = 3) + if GlobalRegion == 3 { + err := getAPIPriceData(urlSE3) + if err != nil { + return + } + + } + // SE4: Södra Sverige/Kalmar (value = 4) + if GlobalRegion == 4 { + err := getAPIPriceData(urlSE4) + if err != nil { + return + } + + } +} + var errStatuscode error = fmt.Errorf("bad status code") var data []GlobalPriceData // Create a list to hold the data json @@ -188,6 +237,12 @@ func initTemplate() components.UnitAsset { Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the temperature the user wants regardless of prices (using a GET request)", } + setRegion := components.Service{ + Definition: "Region", + SubPath: "Region", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the temperature the user wants regardless of prices (using a GET request)", + } return &UnitAsset{ //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) @@ -201,6 +256,7 @@ func initTemplate() components.UnitAsset { DesiredTemp: 0, // Desired temp calculated by system Period: 15, UserTemp: 0, + Region: 1, // maps the provided services from above ServicesMap: components.Services{ @@ -211,6 +267,7 @@ func initTemplate() components.UnitAsset { setSEKPrice.SubPath: &setSEKPrice, setDesiredTemp.SubPath: &setDesiredTemp, setUserTemp.SubPath: &setUserTemp, + setRegion.SubPath: &setRegion, }, } } @@ -247,6 +304,7 @@ func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Serv DesiredTemp: uac.DesiredTemp, Period: uac.Period, UserTemp: uac.UserTemp, + Region: uac.Region, CervicesMap: components.Cervices{ t.Name: t, }, @@ -362,6 +420,20 @@ func (ua *UnitAsset) getUserTemp() (f forms.SignalA_v1a) { f.Timestamp = time.Now() return f } +func (ua *UnitAsset) setRegion(f forms.SignalA_v1a) { + ua.Region = f.Value + GlobalRegion = ua.Region + switchRegion() + +} + +func (ua *UnitAsset) getRegion() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.Region + f.Unit = "---" + f.Timestamp = time.Now() + return f +} // feedbackLoop is THE control loop (IPR of the system) func (ua *UnitAsset) feedbackLoop(ctx context.Context) { @@ -383,6 +455,7 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { // this function adjust and sends a new desierd temprature to the zigbee system // get the current best temperature func (ua *UnitAsset) processFeedbackLoop() { + ua.Region = GlobalRegion // extracts the electricity price depending on the current time and updates globalPrice now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) for _, i := range data { From a728e2f46c72a4ff0d2c3f44968674af1c78f4ae Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 12 Feb 2025 12:00:26 +0100 Subject: [PATCH 084/102] cleand up the new switchRegion function and added some comments --- Comfortstat/things.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 917671a..2b78259 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -90,8 +90,8 @@ func priceFeedbackLoop() { } } +// This function checks if the user has changed price-region and then calls the getAPIPriceData function which gets the right pricedata func switchRegion() { - //url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) urlSE1 := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) urlSE2 := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE2.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) urlSE3 := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE3.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) @@ -430,7 +430,7 @@ func (ua *UnitAsset) setRegion(f forms.SignalA_v1a) { func (ua *UnitAsset) getRegion() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.Region - f.Unit = "---" + f.Unit = "RegionPoint" f.Timestamp = time.Now() return f } From 5e304d51c167dee6b8eef980a188aa670f1014d7 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 13 Feb 2025 10:13:37 +0100 Subject: [PATCH 085/102] Added tests for the newly implemented features(Usertemp and REgion controll --- Comfortstat/Comfortstat_test.go | 180 +++++++++++++++++++++++++++++--- Comfortstat/things_test.go | 71 ++++++++++--- 2 files changed, 222 insertions(+), 29 deletions(-) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index 88014c8..88dfe49 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -207,9 +207,9 @@ func TestHttpSetMinPrice(t *testing.T) { // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/MinPrice", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/MinPrice", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. ua.httpSetMinPrice(w, r) // save the rsponse resp = w.Result() @@ -218,7 +218,7 @@ func TestHttpSetMinPrice(t *testing.T) { } //Good test case: GET w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "localhost:8670/Comfortstat/Set%20Values/MinPrice", nil) + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/MinPrice", nil) goodStatusCode = 200 ua.httpSetMinPrice(w, r) @@ -245,7 +245,7 @@ func TestHttpSetMinPrice(t *testing.T) { // force the case to hit default statement but alter the method w = httptest.NewRecorder() - r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/MinPrice", nil) + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/MinPrice", nil) ua.httpSetMinPrice(w, r) //save the response resp = w.Result() @@ -260,9 +260,9 @@ func TestHttpSetMaxPrice(t *testing.T) { // creates a fake request body with JSON data w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 2, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/MaxPrice", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + fakebody := bytes.NewReader([]byte(`{"value": 2, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/MaxPrice", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. goodStatusCode := 200 ua.httpSetMaxPrice(w, r) @@ -275,9 +275,9 @@ func TestHttpSetMaxPrice(t *testing.T) { // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/MaxPrice", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + fakebody = bytes.NewReader([]byte(`{"123, "unit": "SEK", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/MaxPrice", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. ua.httpSetMaxPrice(w, r) // save the rsponse and read the body @@ -331,9 +331,9 @@ func TestHttpSetDesiredTemp(t *testing.T) { // creates a fake request body with JSON data w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 0, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/DesiredTemp", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + fakebody := bytes.NewReader([]byte(`{"value": 0, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/DesiredTemp", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. goodStatusCode := 200 ua.httpSetDesiredTemp(w, r) @@ -348,9 +348,9 @@ func TestHttpSetDesiredTemp(t *testing.T) { // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "localhost:8670/Comfortstat/Set%20Values/DesiredTemp", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/DesiredTemp", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. ua.httpSetDesiredTemp(w, r) // save the rsponse and read the body @@ -397,3 +397,149 @@ func TestHttpSetDesiredTemp(t *testing.T) { t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) } } + +func TestHttpSetUserTemp(t *testing.T) { + ua := initTemplate().(*UnitAsset) + //Godd test case: PUT + + // creates a fake request body with JSON data + w := httptest.NewRecorder() + fakebody := bytes.NewReader([]byte(`{"value": 0, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/userTemp", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + + ua.httpSetUserTemp(w, r) + + // save the rsponse and read the body + resp := w.Result() + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) + } + + //BAD case: PUT, if the fake body is formatted incorrectly + + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody = bytes.NewReader([]byte(`{"123, "unit": "Celsius", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/userTemp", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + + ua.httpSetUserTemp(w, r) + // save the rsponse and read the body + resp = w.Result() + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) + } + //Good test case: GET + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/userTemp", nil) + goodStatusCode = 200 + ua.httpSetUserTemp(w, r) + + // save the rsponse and read the body + + resp = w.Result() + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + // this is a simple check if the JSON response contains the specific value/unit/version + value := strings.Contains(string(body), `"value": 0`) + unit := strings.Contains(string(body), `"unit": "Celsius"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if value != true { + t.Errorf("expected the statment to be true!") + } + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + if version != true { + t.Errorf("expected the version statment to be true!") + } + // bad test case: default part of code + + // force the case to hit default statement but alter the method + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/userTemp", nil) + // calls the method and extracts the response and save is in resp for the upcoming tests + ua.httpSetUserTemp(w, r) + resp = w.Result() + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + } +} + +func TestHttpSetRegion(t *testing.T) { + ua := initTemplate().(*UnitAsset) + //Godd test case: PUT + + // creates a fake request body with JSON data + w := httptest.NewRecorder() + fakebody := bytes.NewReader([]byte(`{"value": 1, "unit": "RegionPoint", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/Region", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + + ua.httpSetRegion(w, r) + + // save the rsponse and read the body + resp := w.Result() + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) + } + + //BAD case: PUT, if the fake body is formatted incorrectly + + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody = bytes.NewReader([]byte(`{"123, "unit": "RegionPoint", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/Region", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + + ua.httpSetRegion(w, r) + // save the rsponse and read the body + resp = w.Result() + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) + } + //Good test case: GET + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "http://localhost:8670/Comfortstat/Set%20Values/Region", nil) + goodStatusCode = 200 + ua.httpSetRegion(w, r) + + // save the rsponse and read the body + + resp = w.Result() + if resp.StatusCode != goodStatusCode { + t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + // this is a simple check if the JSON response contains the specific value/unit/version + value := strings.Contains(string(body), `"value": 1`) + unit := strings.Contains(string(body), `"unit": "RegionPoint"`) + version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) + + if value != true { + t.Errorf("expected the statment to be true!") + } + if unit != true { + t.Errorf("expected the unit statement to be true!") + } + if version != true { + t.Errorf("expected the version statment to be true!") + } + // bad test case: default part of code + + // force the case to hit default statement but alter the method + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/Region", nil) + // calls the method and extracts the response and save is in resp for the upcoming tests + ua.httpSetRegion(w, r) + resp = w.Result() + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + } +} diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go index 0a6f3fb..640d90a 100644 --- a/Comfortstat/things_test.go +++ b/Comfortstat/things_test.go @@ -116,43 +116,80 @@ func TestSetmethods(t *testing.T) { MinTempInputSignal := forms.SignalA_v1a{ Value: 1.0, } - MaxTempInputSignal := forms.SignalA_v1a{ - Value: 29.0, - } - MinPriceInputSignal := forms.SignalA_v1a{ - Value: 2.0, - } - MaxPriceInputSignal := forms.SignalA_v1a{ - Value: 12.0, - } - DesTempInputSignal := forms.SignalA_v1a{ - Value: 23.7, - } //call and test MinTemp asset.setMinTemp(MinTempInputSignal) if asset.MinTemp != 1.0 { t.Errorf("expected MinTemp to be 1.0, got %f", asset.MinTemp) } + // Simulate the input signals + MaxTempInputSignal := forms.SignalA_v1a{ + Value: 29.0, + } // call and test MaxTemp asset.setMaxTemp(MaxTempInputSignal) if asset.MaxTemp != 29.0 { t.Errorf("expected MaxTemp to be 25.0, got %f", asset.MaxTemp) } + // Simulate the input signals + MinPriceInputSignal := forms.SignalA_v1a{ + Value: 2.0, + } //call and test MinPrice asset.setMinPrice(MinPriceInputSignal) if asset.MinPrice != 2.0 { t.Errorf("expected MinPrice to be 2.0, got %f", asset.MinPrice) } + // Simulate the input signals + MaxPriceInputSignal := forms.SignalA_v1a{ + Value: 12.0, + } //call and test MaxPrice asset.setMaxPrice(MaxPriceInputSignal) if asset.MaxPrice != 12.0 { t.Errorf("expected MaxPrice to be 12.0, got %f", asset.MaxPrice) } + // Simulate the input signals + DesTempInputSignal := forms.SignalA_v1a{ + Value: 23.7, + } // call and test DesiredTemp asset.setDesiredTemp(DesTempInputSignal) if asset.DesiredTemp != 23.7 { t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.DesiredTemp) } + // Simulate the input signal and call the set method for the check + RegionInputSignalSE2 := forms.SignalA_v1a{ + Value: 2, + } + asset.setRegion(RegionInputSignalSE2) + if asset.Region != 2.0 { + t.Errorf("expected Region to be SE2 (2), got %f", asset.Region) + } + // Simulate the input signal and call the set method for the check + RegionInputSignalSE3 := forms.SignalA_v1a{ + Value: 3, + } + asset.setRegion(RegionInputSignalSE3) + if asset.Region != 3.0 { + t.Errorf("expected Region to be SE3 (3), got %f", asset.Region) + } + // Simulate the input signal and call the set method for the check + RegionInputSignalSE1 := forms.SignalA_v1a{ + Value: 1, + } + asset.setRegion(RegionInputSignalSE1) + if asset.Region != 1.0 { + t.Errorf("expected Region to be SE1 (1), got %f", asset.Region) + } + // Simulate the input signal and call the set method for the check + RegionInputSignalSE4 := forms.SignalA_v1a{ + Value: 4, + } + asset.setRegion(RegionInputSignalSE4) + if asset.Region != 4.0 { + t.Errorf("expected Region to be SE4 (4), got %f", asset.Region) + } + } func TestGetMethods(t *testing.T) { @@ -212,6 +249,16 @@ func TestGetMethods(t *testing.T) { if result6.Value != uasset.SEKPrice { t.Errorf("expected electric price is to be %v, got %v", uasset.SEKPrice, result6.Value) } + ////USertemp//// + result7 := uasset.getUserTemp() + if result7.Value != uasset.UserTemp { + t.Errorf("expected the Usertemp to be %v, got %v", uasset.UserTemp, result7.Value) + } + ////Region//// + result8 := uasset.getRegion() + if result8.Value != uasset.Region { + t.Errorf("expected the Region to be %v, got %v", uasset.Region, result8.Value) + } } func TestInitTemplate(t *testing.T) { From bb0a152437f5314985e6f60a56ad80bdb1a99593 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 13 Feb 2025 10:14:33 +0100 Subject: [PATCH 086/102] Cleand up the new funtions that have been implemented --- Comfortstat/things.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 2b78259..5bd2b42 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -103,7 +103,6 @@ func switchRegion() { if err != nil { return } - } // SE2: Norra MellanSverige/Sundsvall (value = 2) if GlobalRegion == 2 { @@ -111,7 +110,6 @@ func switchRegion() { if err != nil { return } - } // SE3: Södra MellanSverige/Stockholm (value = 3) if GlobalRegion == 3 { @@ -119,7 +117,6 @@ func switchRegion() { if err != nil { return } - } // SE4: Södra Sverige/Kalmar (value = 4) if GlobalRegion == 4 { @@ -127,7 +124,6 @@ func switchRegion() { if err != nil { return } - } } @@ -403,7 +399,6 @@ func (ua *UnitAsset) getDesiredTemp() (f forms.SignalA_v1a) { func (ua *UnitAsset) setDesiredTemp(f forms.SignalA_v1a) { ua.DesiredTemp = f.Value - log.Printf("new desired temperature: %.1f", f.Value) } func (ua *UnitAsset) setUserTemp(f forms.SignalA_v1a) { @@ -424,7 +419,6 @@ func (ua *UnitAsset) setRegion(f forms.SignalA_v1a) { ua.Region = f.Value GlobalRegion = ua.Region switchRegion() - } func (ua *UnitAsset) getRegion() (f forms.SignalA_v1a) { From 9ff159bd3ef1d39caade12d778f10ed1dbba5f8b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 13:26:18 +0100 Subject: [PATCH 087/102] Removes uninteresting log line --- ZigBeeValve/ZigBeeValve.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 91d0d5b..ac0e411 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -101,7 +101,6 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { if rsc.Model == "ZHAThermostat" { err = rsc.sendSetPoint() if err != nil { - log.Println("Error sending setpoint:", err) http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) return } From e3b9bc19c543f5370b9209cd6f7556b521a65dfd Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 24 Jan 2025 18:37:08 +0100 Subject: [PATCH 088/102] Adds new linter --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6b6d159..6040736 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ lint: test -z $$(gofmt -l .) || (echo "Code isn't gofmt'ed!" && exit 1) go vet $$(go list ./... | grep -v /tmp) gosec -quiet -fmt=golint -exclude-dir="tmp" ./... + pointerinterface ./... # Generate pretty coverage report analyse: @@ -26,6 +27,7 @@ deps: go mod download go install github.com/securego/gosec/v2/cmd/gosec@latest go install github.com/fzipp/gocyclo/cmd/gocyclo@latest + go install code.larus.se/lmas/pointerinterface@latest # Show documentation of public parts of package, in the current dir docs: @@ -41,4 +43,3 @@ build: clean: go clean rm .cover.out cover.html - # TODO: add raspberrypi bins From 181419a29f39de8f4b56fba6259bab9a3f1839ba Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 12 Feb 2025 12:34:25 +0100 Subject: [PATCH 089/102] Ignores test files while running gocyclo --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6040736..6f1e867 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ analyse: @echo -e "\nCOVERAGE\n====================" go tool cover -func=.cover.out @echo -e "\nCYCLOMATIC COMPLEXITY\n====================" - gocyclo -avg -top 10 . + gocyclo -avg -top 10 -ignore test.go . # Updates 3rd party packages and tools deps: From 007791e589c43c72b0a08215818d54f44be58e2a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 13:47:31 +0100 Subject: [PATCH 090/102] Adds spellchecking tool --- .github/workflows/main.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d95f148..6c94273 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Tests and Linters +name: Linters, Spellcheck and Tests on: push: @@ -7,7 +7,7 @@ on: jobs: Linters: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 2 steps: - uses: actions/checkout@v4 - name: Setup go @@ -19,9 +19,16 @@ jobs: - name: Run linters run: make lint + Spellcheck: + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@v1.29.7 + Tests: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 2 steps: - uses: actions/checkout@v4 - name: Setup go From d726bc49f19816dafd31912e1abaacb9b7e2b5b6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 13:57:43 +0100 Subject: [PATCH 091/102] Fixes all the speeeeling errors --- Comfortstat/Comfortstat.go | 22 ++++++++--------- Comfortstat/Comfortstat_test.go | 32 ++++++++++++------------- Comfortstat/things.go | 16 ++++++------- Comfortstat/things_test.go | 42 ++++++++++++++++----------------- ZigBeeValve/ZigBeeValve.go | 6 ++--- ZigBeeValve/thing.go | 20 ++++++++-------- ZigBeeValve/thing_test.go | 12 +++++----- ZigBeeValve/zigbee_test.go | 4 ++-- 8 files changed, 77 insertions(+), 77 deletions(-) diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index 9f4a6ff..abb2bb8 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -20,7 +20,7 @@ func main() { // instantiate the System sys := components.NewSystem("Comfortstat", ctx) - // Instatiate the Capusle + // Instantiate the Capsule sys.Husk = &components.Husk{ Description: " is a controller for a consumed servo motor position based on a consumed temperature", Certificate: "ABCD", @@ -31,7 +31,7 @@ func main() { // instantiate a template unit asset assetTemplate := initTemplate() - // Calling initAPI() starts the pricefeedbackloop that fetches the current electrisity price for the particular hour + // Calling initAPI() starts the pricefeedbackloop that fetches the current electricity price for the particular hour initAPI() time.Sleep(1 * time.Second) assetName := assetTemplate.GetName() @@ -69,7 +69,7 @@ func main() { time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end } -// Serving handles the resources services. NOTE: it exepcts those names from the request URL path +// Serving handles the resources services. NOTE: it expects those names from the request URL path func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { switch servicePath { case "MinTemperature": @@ -103,7 +103,7 @@ func (rsc *UnitAsset) httpSetSEKPrice(w http.ResponseWriter, r *http.Request) { } } -// All these functions below handles HTTP "PUT" or "GET" requests to modefy or retrieve the MAX/MIN temprature/price and desierd temprature +// All these functions below handles HTTP "PUT" or "GET" requests to modefy or retrieve the MAX/MIN temprature/price and desierd temperature // For the PUT case - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, // call the set functions in things.go with the value witch updates the value in the struct func (rsc *UnitAsset) httpSetMinTemp(w http.ResponseWriter, r *http.Request) { @@ -112,7 +112,7 @@ func (rsc *UnitAsset) httpSetMinTemp(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { //log.Println("Error with the setting request of the position ", err) - http.Error(w, "request incorreclty formated", http.StatusBadRequest) + http.Error(w, "request incorrectly formatted", http.StatusBadRequest) return } @@ -130,7 +130,7 @@ func (rsc *UnitAsset) httpSetMaxTemp(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { //log.Println("Error with the setting request of the position ", err) - http.Error(w, "request incorreclty formated", http.StatusBadRequest) + http.Error(w, "request incorrectly formatted", http.StatusBadRequest) return } rsc.setMaxTemp(sig) @@ -148,7 +148,7 @@ func (rsc *UnitAsset) httpSetMinPrice(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { //log.Println("Error with the setting request of the position ", err) - http.Error(w, "request incorreclty formated", http.StatusBadRequest) + http.Error(w, "request incorrectly formatted", http.StatusBadRequest) return } rsc.setMinPrice(sig) @@ -167,7 +167,7 @@ func (rsc *UnitAsset) httpSetMaxPrice(w http.ResponseWriter, r *http.Request) { sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { //log.Println("Error with the setting request of the position ", err) - http.Error(w, "request incorreclty formated", http.StatusBadRequest) + http.Error(w, "request incorrectly formatted", http.StatusBadRequest) return } rsc.setMaxPrice(sig) @@ -186,7 +186,7 @@ func (rsc *UnitAsset) httpSetDesiredTemp(w http.ResponseWriter, r *http.Request) sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { //log.Println("Error with the setting request of the position ", err) - http.Error(w, "request incorreclty formated", http.StatusBadRequest) + http.Error(w, "request incorrectly formatted", http.StatusBadRequest) return } rsc.setDesiredTemp(sig) @@ -204,7 +204,7 @@ func (rsc *UnitAsset) httpSetUserTemp(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - http.Error(w, "request incorrectly formated", http.StatusBadRequest) + http.Error(w, "request incorrectly formatted", http.StatusBadRequest) return } rsc.setUserTemp(sig) @@ -221,7 +221,7 @@ func (rsc *UnitAsset) httpSetRegion(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - http.Error(w, "request incorrectly formated", http.StatusBadRequest) + http.Error(w, "request incorrectly formatted", http.StatusBadRequest) return } rsc.setRegion(sig) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index 88dfe49..9288014 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -29,13 +29,13 @@ func TestHttpSetSEKPrice(t *testing.T) { version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) // check results from above if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // Bad test case: default part of code w = httptest.NewRecorder() @@ -97,13 +97,13 @@ func TestHttpSetMinTemp(t *testing.T) { version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) // check the result from above if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // bad test case: default part of code // force the case to hit default statement but alter the method @@ -165,13 +165,13 @@ func TestHttpSetMaxTemp(t *testing.T) { unit := strings.Contains(string(body), `"unit": "Celsius"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // bad test case: default part of code @@ -233,13 +233,13 @@ func TestHttpSetMinPrice(t *testing.T) { unit := strings.Contains(string(body), `"unit": "SEK"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // bad test case: default part of code @@ -303,13 +303,13 @@ func TestHttpSetMaxPrice(t *testing.T) { version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // bad test case: default part of code @@ -377,13 +377,13 @@ func TestHttpSetDesiredTemp(t *testing.T) { version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // bad test case: default part of code @@ -450,13 +450,13 @@ func TestHttpSetUserTemp(t *testing.T) { version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // bad test case: default part of code @@ -523,13 +523,13 @@ func TestHttpSetRegion(t *testing.T) { version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { - t.Errorf("expected the statment to be true!") + t.Errorf("expected the statement to be true!") } if unit != true { t.Errorf("expected the unit statement to be true!") } if version != true { - t.Errorf("expected the version statment to be true!") + t.Errorf("expected the version statement to be true!") } // bad test case: default part of code diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 5bd2b42..877a9a9 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -130,10 +130,10 @@ func switchRegion() { var errStatuscode error = fmt.Errorf("bad status code") var data []GlobalPriceData // Create a list to hold the data json -// This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then prosess it and updates globalPrice +// This function fetches the current electricity price from "https://www.elprisetjustnu.se/elpris-api", then process it and updates globalPrice func getAPIPriceData(apiURL string) error { //Validate the URL// - parsedURL, err := url.Parse(apiURL) // ensures the string is a valid URL, .schema and .Host checks prevent emty or altered URL + parsedURL, err := url.Parse(apiURL) // ensures the string is a valid URL, .schema and .Host checks prevent empty or altered URL if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { return errors.New("The URL is invalid") } @@ -248,7 +248,7 @@ func initTemplate() components.UnitAsset { MinPrice: 1.0, // Minimum price allowed MaxPrice: 2.0, // Maximum price allowed MinTemp: 20.0, // Minimum temperature - MaxTemp: 25.0, // Maximum temprature allowed + MaxTemp: 25.0, // Maximum temperature allowed DesiredTemp: 0, // Desired temp calculated by system Period: 15, UserTemp: 0, @@ -331,7 +331,7 @@ func (ua *UnitAsset) getSEKPrice() (f forms.SignalA_v1a) { return f } -//Get and set- metods for MIN/MAX price/temp and desierdTemp +//Get and set- methods for MIN/MAX price/temp and desierdTemp // getMinPrice is used for reading the current value of MinPrice func (ua *UnitAsset) getMinPrice() (f forms.SignalA_v1a) { @@ -361,7 +361,7 @@ func (ua *UnitAsset) setMaxPrice(f forms.SignalA_v1a) { ua.MaxPrice = f.Value } -// getMinTemp is used for reading the current minimum temerature value +// getMinTemp is used for reading the current minimum temperature value func (ua *UnitAsset) getMinTemp() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.MinTemp @@ -439,14 +439,14 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { for { select { case <-ticker.C: - ua.processFeedbackLoop() // either modifiy processFeedback loop or write a new one + ua.processFeedbackLoop() // either modify processFeedback loop or write a new one case <-ctx.Done(): return } } } -// this function adjust and sends a new desierd temprature to the zigbee system +// this function adjust and sends a new desierd temperature to the zigbee system // get the current best temperature func (ua *UnitAsset) processFeedbackLoop() { ua.Region = GlobalRegion @@ -494,7 +494,7 @@ func (ua *UnitAsset) processFeedbackLoop() { } } -// Calculates the new most optimal temprature (desierdTemp) based on the price/temprature intervalls +// Calculates the new most optimal temperature (desierdTemp) based on the price/temprature intervals // and the current electricity price func (ua *UnitAsset) calculateDesiredTemp() float64 { diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go index 640d90a..d87b971 100644 --- a/Comfortstat/things_test.go +++ b/Comfortstat/things_test.go @@ -26,7 +26,7 @@ func newMockTransport(resp *http.Response) mockTransport { resp: resp, hits: make(map[string]int), } - // Highjack the default http client so no actuall http requests are sent over the network + // Hijack the default http client so no actual http requests are sent over the network http.DefaultClient.Transport = t return t } @@ -155,7 +155,7 @@ func TestSetmethods(t *testing.T) { // call and test DesiredTemp asset.setDesiredTemp(DesTempInputSignal) if asset.DesiredTemp != 23.7 { - t.Errorf("expected Desierd temprature is to be 23.7, got %f", asset.DesiredTemp) + t.Errorf("expected Desierd temperature is to be 23.7, got %f", asset.DesiredTemp) } // Simulate the input signal and call the set method for the check RegionInputSignalSE2 := forms.SignalA_v1a{ @@ -196,7 +196,7 @@ func TestGetMethods(t *testing.T) { uasset := initTemplate().(*UnitAsset) ////MinTemp//// - // check if the value from the struct is the acctual value that the func is getting + // check if the value from the struct is the actual value that the func is getting result := uasset.getMinTemp() if result.Value != uasset.MinTemp { t.Errorf("expected Value of the MinTemp is to be %v, got %v", uasset.MinTemp, result.Value) @@ -215,7 +215,7 @@ func TestGetMethods(t *testing.T) { t.Errorf("expected Unit of the MaxTemp is to be 'Celsius', got %v", result2.Unit) } ////MinPrice//// - // check if the value from the struct is the acctual value that the func is getting + // check if the value from the struct is the actual value that the func is getting result3 := uasset.getMinPrice() if result3.Value != uasset.MinPrice { t.Errorf("expected Value of the minPrice is to be %v, got %v", uasset.MinPrice, result3.Value) @@ -225,7 +225,7 @@ func TestGetMethods(t *testing.T) { t.Errorf("expected Unit to be 'SEK', got %v", result3.Unit) } ////MaxPrice//// - // check if the value from the struct is the acctual value that the func is getting + // check if the value from the struct is the actual value that the func is getting result4 := uasset.getMaxPrice() if result4.Value != uasset.MaxPrice { t.Errorf("expected Value of the maxPrice is to be %v, got %v", uasset.MaxPrice, result4.Value) @@ -235,10 +235,10 @@ func TestGetMethods(t *testing.T) { t.Errorf("expected Unit to be 'SEK', got %v", result4.Unit) } ////DesierdTemp//// - // check if the value from the struct is the acctual value that the func is getting + // check if the value from the struct is the actual value that the func is getting result5 := uasset.getDesiredTemp() if result5.Value != uasset.DesiredTemp { - t.Errorf("expected desired temprature is to be %v, got %v", uasset.DesiredTemp, result5.Value) + t.Errorf("expected desired temperature is to be %v, got %v", uasset.DesiredTemp, result5.Value) } //check that the Unit is correct if result5.Unit != "Celsius" { @@ -275,22 +275,22 @@ func TestInitTemplate(t *testing.T) { } //Services// if Services["SEKPrice"].Definition != "SEKPrice" { - t.Errorf("expected service defenition to be SEKprice") + t.Errorf("expected service definition to be SEKprice") } if Services["MaxTemperature"].Definition != "MaxTemperature" { - t.Errorf("expected service defenition to be MaxTemperature") + t.Errorf("expected service definition to be MaxTemperature") } if Services["MinTemperature"].Definition != "MinTemperature" { - t.Errorf("expected service defenition to be MinTemperature") + t.Errorf("expected service definition to be MinTemperature") } if Services["MaxPrice"].Definition != "MaxPrice" { - t.Errorf("expected service defenition to be MaxPrice") + t.Errorf("expected service definition to be MaxPrice") } if Services["MinPrice"].Definition != "MinPrice" { - t.Errorf("expected service defenition to be MinPrice") + t.Errorf("expected service definition to be MinPrice") } if Services["DesiredTemp"].Definition != "DesiredTemp" { - t.Errorf("expected service defenition to be DesiredTemp") + t.Errorf("expected service definition to be DesiredTemp") } //GetCervice// Cervices := uasset.GetCervices() @@ -311,7 +311,7 @@ func TestNewUnitAsset(t *testing.T) { // instantiate the System sys := components.NewSystem("Comfortstat", ctx) - // Instatiate the Capusle + // Instantiate the Capsule sys.Husk = &components.Husk{ Description: " is a controller for a consumed servo motor position based on a consumed temperature", Certificate: "ABCD", @@ -364,7 +364,7 @@ func TestNewUnitAsset(t *testing.T) { MinPrice: 1.0, // Minimum price allowed MaxPrice: 2.0, // Maximum price allowed MinTemp: 20.0, // Minimum temperature - MaxTemp: 25.0, // Maximum temprature allowed + MaxTemp: 25.0, // Maximum temperature allowed DesiredTemp: 0, // Desired temp calculated by system Period: 15, @@ -393,20 +393,20 @@ func TestCalculateDesiredTemp(t *testing.T) { asset := initTemplate().(*UnitAsset) // calls and saves the value result := asset.calculateDesiredTemp() - // checks if actual calculated value matches the expexted value + // checks if actual calculated value matches the expected value if result != True_result { t.Errorf("Expected calculated temp is %v, got %v", True_result, result) } } -// This test catches the special cases, when the temprature is to be set to the minimum temprature right away +// This test catches the special cases, when the temperature is to be set to the minimum temperature right away func TestSpecialCalculate(t *testing.T) { asset := UnitAsset{ SEKPrice: 3.0, MaxPrice: 2.0, MinTemp: 17.0, } - //call the method and save the result in a varable for testing + //call the method and save the result in a variable for testing result := asset.calculateDesiredTemp() //check the result from the call above if result != asset.MinTemp { @@ -414,7 +414,7 @@ func TestSpecialCalculate(t *testing.T) { } } -// Fuctions that help creating bad body +// Functions that help creating bad body type errReader int var errBodyRead error = fmt.Errorf("bad body read") @@ -453,7 +453,7 @@ func TestGetAPIPriceData(t *testing.T) { `https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), ) - // creates a mock HTTP transport to simulate api respone for the test + // creates a mock HTTP transport to simulate api response for the test newMockTransport(resp) err := getAPIPriceData(url) if err != nil { @@ -493,7 +493,7 @@ func TestGetAPIPriceData(t *testing.T) { resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) newMockTransport(resp) err = getAPIPriceData(url) - // make the check if the unmarshal creats a error + // make the check if the unmarshal creates a error if err == nil { t.Errorf("expected an error, got %v :", err) } diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index ac0e411..d0cd301 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -23,7 +23,7 @@ func main() { // instantiate the System sys := components.NewSystem("ZigBeeHandler", ctx) - // Instatiate the Capusle + // Instantiate the Capsule sys.Husk = &components.Husk{ Description: " is a controller for smart devices connected with a RaspBee II", Certificate: "ABCD", @@ -75,7 +75,7 @@ func main() { time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end } -// Serving handles the resources services. NOTE: it exepcts those names from the request URL path +// Serving handles the resources services. NOTE: it expects those names from the request URL path func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { switch servicePath { case "setpoint": @@ -93,7 +93,7 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - http.Error(w, "Request incorrectly formated", http.StatusBadRequest) + http.Error(w, "Request incorrectly formatted", http.StatusBadRequest) return } diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 59c337c..1bec700 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -71,7 +71,7 @@ func (ua *UnitAsset) GetDetails() map[string][]string { // ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) var _ components.UnitAsset = (*UnitAsset)(nil) -//-------------------------------------Instatiate a unit asset template +//-------------------------------------Instantiate a unit asset template // initTemplate initializes a UnitAsset with default values. func initTemplate() components.UnitAsset { @@ -106,13 +106,13 @@ func initTemplate() components.UnitAsset { return uat } -//-------------------------------------Instatiate the unit assets based on configuration +//-------------------------------------Instantiate the unit assets based on configuration // newResource creates the resource with its pointers and channels based on the configuration using the tConfig structs // This is a startup function that's used to initiate the unit assets declared in the systemconfig.json, the function // that is returned is later used to send a setpoint/start a goroutine depending on model of the unitasset func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { - // deterimine the protocols that the system supports + // determine the protocols that the system supports sProtocols := components.SProtocols(sys.Husk.ProtoPort) // instantiate the consumed services @@ -121,7 +121,7 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi Protos: sProtocols, Url: make([]string, 0), } - // intantiate the unit asset + // instantiate the unit asset ua := &UnitAsset{ Name: uac.Name, Owner: sys, @@ -150,18 +150,18 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi // Get correct index in list returned by api/sensors to make sure we always change correct device err := ua.getConnectedUnits("sensors") if err != nil { - log.Println("Error occured during startup, while calling getConnectedUnits:", err) + log.Println("Error occurred during startup, while calling getConnectedUnits:", err) } err = ua.sendSetPoint() if err != nil { - log.Println("Error occured during startup, while calling sendSetPoint():", err) + log.Println("Error occurred during startup, while calling sendSetPoint():", err) // TODO: Turn off system if this startup() fails? } } else if ua.Model == "Smart plug" { // Get correct index in list returned by api/lights to make sure we always change correct device err := ua.getConnectedUnits("lights") if err != nil { - log.Println("Error occured during startup, while calling getConnectedUnits:", err) + log.Println("Error occurred during startup, while calling getConnectedUnits:", err) } // Not all smart plugs should be handled by the feedbackloop, some should be handled by a switch if ua.Period != 0 { @@ -205,12 +205,12 @@ func (ua *UnitAsset) processFeedbackLoop() { if tup.Value < ua.Setpt { err = ua.toggleState(true) if err != nil { - log.Println("Error occured while toggling state to true: ", err) + log.Println("Error occurred while toggling state to true: ", err) } } else { err = ua.toggleState(false) if err != nil { - log.Println("Error occured while toggling state to false: ", err) + log.Println("Error occurred while toggling state to false: ", err) } } } @@ -390,7 +390,7 @@ func initWebsocketClient(ctx context.Context) (err error) { var bodyString map[string]interface{} err = json.Unmarshal(data, &bodyString) // Unmarshal body into json, easier to be able to point to specific data with ".example" if err != nil { - log.Println("Error while unmarshaling data:", err) + log.Println("Error while unmarshalling data:", err) return } log.Println("Read from websocket:", bodyString) diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index e4f6aab..ee31ba7 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -30,7 +30,7 @@ func newMockTransport(resp *http.Response, retErr bool, err error) mockTransport hits: make(map[string]int), err: err, } - // Highjack the default http client so no actuall http requests are sent over the network + // Hijack the default http client so no actual http requests are sent over the network http.DefaultClient.Transport = t return t } @@ -194,7 +194,7 @@ func TestFindGateway(t *testing.T) { newMockTransport(resp, false, fmt.Errorf("Test error")) err = findGateway() if err == nil { - t.Error("Error expcted during unmarshalling, got nil instead", err) + t.Error("Error expected during unmarshalling, got nil instead", err) } // Statuscode > 299, have to make changes to mockTransport to test this @@ -322,7 +322,7 @@ func TestGetConnectedUnits(t *testing.T) { // Test function err = ua.getConnectedUnits(ua.Model) if err != nil { - t.Error("Expected no errors, error occured:", err) + t.Error("Expected no errors, error occurred:", err) } // --- Bad statuscode --- @@ -381,7 +381,7 @@ func TestCreateRequest(t *testing.T) { _, err := createRequest(data, apiURL) if err != nil { - t.Error("Error occured, expected none") + t.Error("Error occurred, expected none") } _, err = createRequest(data, brokenURL) @@ -410,7 +410,7 @@ func TestSendRequest(t *testing.T) { req, _ := createRequest(s, apiURL) err := sendRequest(req) if err != nil { - t.Error("Expected no errors, error occured:", err) + t.Error("Expected no errors, error occurred:", err) } // Break defaultClient.Do() @@ -430,7 +430,7 @@ func TestSendRequest(t *testing.T) { err = sendRequest(req) if err == nil { - t.Error("Expected errors, no error occured:") + t.Error("Expected errors, no error occurred:") } // Error StatusCode diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index ef08958..c9e4f72 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -33,13 +33,13 @@ func TestSetpt(t *testing.T) { version := strings.Contains(string(stringBody), `"version": "SignalA_v1.0"`) // Check that above statements are true if value != true { - t.Errorf("Good GET: The value statment should be true!") + t.Errorf("Good GET: The value statement should be true!") } if unit != true { t.Errorf("Good GET: Expected the unit statement to be true!") } if version != true { - t.Errorf("Good GET: Expected the version statment to be true!") + t.Errorf("Good GET: Expected the version statement to be true!") } // --- Bad test case: Default part of code (faulty http method) --- From 33880a8dc16151025bc6314d80eaed0cbef7c3eb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 14:09:26 +0100 Subject: [PATCH 092/102] Adds manual spellcheck to makefile --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Makefile b/Makefile index 6f1e867..8597a08 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,13 @@ lint: gosec -quiet -fmt=golint -exclude-dir="tmp" ./... pointerinterface ./... +# Runs spellchecker on the code and comments +# This requires this tool to be installed from https://github.com/crate-ci/typos?tab=readme-ov-file +# Example installation: +# cargo install typos-cli +spellcheck: + typos . + # Generate pretty coverage report analyse: go tool cover -html=".cover.out" -o="cover.html" From 619780a10b4fe88b51b029e98934ddf8f5970e52 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 14:26:20 +0100 Subject: [PATCH 093/102] Updates system to reflect latest updates in dev branch --- collector/collect_test.go | 21 ++++++++++---------- collector/unitasset.go | 41 +++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/collector/collect_test.go b/collector/collect_test.go index 0bbb814..ab6dcda 100644 --- a/collector/collect_test.go +++ b/collector/collect_test.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "log" "net/http" "strings" "testing" @@ -40,7 +39,7 @@ func newMockTransport() mockTransport { } func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - log.Println("HIJACK:", req.URL.String()) + // log.Println("HIJACK:", req.URL.String()) // t.hits[req.URL.Hostname()] += 1 // if t.err != nil { // return nil, t.err @@ -53,11 +52,11 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er // t.resp.Request = req // return t.resp, nil - b, err := io.ReadAll(req.Body) - if err != nil { - return - } - fmt.Println(string(b)) + // b, err := io.ReadAll(req.Body) + // if err != nil { + // return + // } + // fmt.Println(string(b)) return &http.Response{ Request: req, @@ -69,10 +68,10 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er const mockBodyType string = "application/json" var mockStates = map[string]string{ - "temperature": `{ "value": 0, "unit": "Celcius", "timestamp": "%s", "version": "SignalA_v1.0" }`, - "SEK_price": `{ "value": 0.10403, "unit": "SEK", "timestamp": "%s", "version": "SignalA_v1.0" }`, - "desired_temp": `{ "value": 25, "unit": "Celsius", "timestamp": "%s", "version": "SignalA_v1.0" }`, - "setpoint": `{ "value": 20, "unit": "Celsius", "timestamp": "%s", "version": "SignalA_v1.0" }`, + "temperature": `{ "value": 0, "unit": "Celcius", "timestamp": "%s", "version": "SignalA_v1.0" }`, + "SEKPrice": `{ "value": 0.10403, "unit": "SEK", "timestamp": "%s", "version": "SignalA_v1.0" }`, + "DesiredTemp": `{ "value": 25, "unit": "Celsius", "timestamp": "%s", "version": "SignalA_v1.0" }`, + "setpoint": `{ "value": 20, "unit": "Celsius", "timestamp": "%s", "version": "SignalA_v1.0" }`, } func mockGetState(c *components.Cervice, s *components.System) (f forms.Form, err error) { diff --git a/collector/unitasset.go b/collector/unitasset.go index ef49709..34f68cf 100644 --- a/collector/unitasset.go +++ b/collector/unitasset.go @@ -32,9 +32,11 @@ type unitAsset struct { ServicesMap components.Services `json:"-"` // Services provided to consumers CervicesMap components.Cervices `json:"-"` // Services being consumed - InfluxDBHost string `json:"influxdb_host"` // IP:port addr to the influxdb server - InfluxDBToken string `json:"influxdb_token"` // Auth token - CollectionPeriod int `json:"collection_period"` // Period (in seconds) between each data collection + InfluxDBHost string `json:"influxdb_host"` // IP:port addr to the influxdb server + InfluxDBToken string `json:"influxdb_token"` // Auth token + InfluxDBOrganisation string `json:"influxdb_organisation"` + InfluxDBBucket string `json:"influxdb_bucket"` // Data bucket + CollectionPeriod int `json:"collection_period"` // Period (in seconds) between each data collection // Mockable function for getting states from the consumed services. apiGetState func(*components.Cervice, *components.System) (forms.Form, error) @@ -81,16 +83,18 @@ func initTemplate() *unitAsset { Name: uaName, Details: map[string][]string{"Location": {"Kitchen"}}, - InfluxDBHost: "http://localhost:8086", - InfluxDBToken: "insert secret token here", - CollectionPeriod: 30, + InfluxDBHost: "http://localhost:8086", + InfluxDBToken: "insert secret token here", + InfluxDBOrganisation: "organisation", + InfluxDBBucket: "arrowhead", + CollectionPeriod: 30, } } var consumeServices []string = []string{ "temperature", - "SEK_price", - "desired_temp", + "SEKPrice", + "DesiredTemp", "setpoint", } @@ -102,11 +106,9 @@ var consumeServices []string = []string{ // func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func() error) { // func newUnitAsset(uac unitAsset, sys *components.System, servs []components.Service) *unitAsset { func newUnitAsset(uac unitAsset, sys *system, servs []components.Service) *unitAsset { - // client := influxdb2.NewClient(uac.InfluxDBHost, uac.InfluxDBToken) - ls := uint(len(consumeServices) + 1) client := influxdb2.NewClientWithOptions( uac.InfluxDBHost, uac.InfluxDBToken, - influxdb2.DefaultOptions().SetBatchSize(ls).SetHTTPClient(http.DefaultClient), + influxdb2.DefaultOptions().SetHTTPClient(http.DefaultClient), ) ua := &unitAsset{ @@ -116,14 +118,15 @@ func newUnitAsset(uac unitAsset, sys *system, servs []components.Service) *unitA // ServicesMap: components.CloneServices(servs), // TODO: not required? CervicesMap: components.Cervices{}, - InfluxDBHost: uac.InfluxDBHost, - InfluxDBToken: uac.InfluxDBToken, - CollectionPeriod: uac.CollectionPeriod, + InfluxDBHost: uac.InfluxDBHost, + InfluxDBToken: uac.InfluxDBToken, + InfluxDBOrganisation: uac.InfluxDBOrganisation, + InfluxDBBucket: uac.InfluxDBBucket, + CollectionPeriod: uac.CollectionPeriod, - apiGetState: usecases.GetState, - // influx: influxdb2.NewClient(uac.InfluxDBHost, uac.InfluxDBToken), + apiGetState: usecases.GetState, influx: client, - influxWriter: client.WriteAPI("organisation", "bucket"), + influxWriter: client.WriteAPI(uac.InfluxDBOrganisation, uac.InfluxDBBucket), } // TODO: handle influx write errors or don't care? @@ -182,7 +185,7 @@ func (ua *unitAsset) cleanup() { } func (ua *unitAsset) collectAllServices() (err error) { - log.Println("tick") // TODO + // log.Println("tick") // TODO var wg sync.WaitGroup for _, service := range consumeServices { @@ -211,7 +214,7 @@ func (ua *unitAsset) collectService(service string) (err error) { err = fmt.Errorf("bad form version: %s", f.FormVersion()) return } - fmt.Println(s) // TODO + // fmt.Println(s) // TODO p := influxdb2.NewPoint( service, From 88abfe1d6f837358c2d23bf1f30a92b901a4104c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 14:39:59 +0100 Subject: [PATCH 094/102] Adds spellcheck config and ignore a word --- _typos.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 _typos.toml diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..8ff0764 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,4 @@ +[default.extend-words] +# This spelling error is caused in the mbaigo systems +Celcius = "Celcius" + From b521dd042b2df33b802c547bc2b50c7bdf94905d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 14:40:30 +0100 Subject: [PATCH 095/102] Fixes some small typos (only a few compared to the others huehue) --- collector/collect_test.go | 2 +- collector/system.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/collector/collect_test.go b/collector/collect_test.go index ab6dcda..2cb08f5 100644 --- a/collector/collect_test.go +++ b/collector/collect_test.go @@ -33,7 +33,7 @@ func newMockTransport() mockTransport { // returnError: retErr, // resp: resp, } - // Highjack the default http client so no actuall http requests are sent over the network + // Hijack the default http client so no actual http requests are sent over the network http.DefaultClient.Transport = t return t } diff --git a/collector/system.go b/collector/system.go index 0f17b21..5cc605f 100644 --- a/collector/system.go +++ b/collector/system.go @@ -67,7 +67,7 @@ func (sys *system) loadConfiguration() { // If the file is missing, a new config will be created and an error is returned here. if err != nil { // TODO: it would had been nice to catch the exact error for "created config.." - // and not display it as an actuall error, per se. + // and not display it as an actual error, per se. log.Fatalf("Error while reading configuration: %v\n", err) } From 54d1fa1efdfed589425261b9a465534bd83e1728 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 15:13:44 +0100 Subject: [PATCH 096/102] Fixes some small things in comfortstat --- Comfortstat/Comfortstat_test.go | 12 ++++-------- Comfortstat/things.go | 9 ++++----- Comfortstat/things_test.go | 5 +++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Comfortstat/Comfortstat_test.go b/Comfortstat/Comfortstat_test.go index 9288014..a6ec374 100644 --- a/Comfortstat/Comfortstat_test.go +++ b/Comfortstat/Comfortstat_test.go @@ -477,9 +477,9 @@ func TestHttpSetRegion(t *testing.T) { // creates a fake request body with JSON data w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 1, "unit": "RegionPoint", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/Region", fakebody) // simulating a put request from a user to update the min temp - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + fakebody := bytes.NewReader([]byte(`{"value": 1, "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/Region", fakebody) // simulating a put request from a user to update the min temp + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. goodStatusCode := 200 ua.httpSetRegion(w, r) @@ -494,7 +494,7 @@ func TestHttpSetRegion(t *testing.T) { // creates a fake request body with JSON data w = httptest.NewRecorder() - fakebody = bytes.NewReader([]byte(`{"123, "unit": "RegionPoint", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + fakebody = bytes.NewReader([]byte(`{"123, "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read r = httptest.NewRequest("PUT", "http://localhost:8670/Comfortstat/Set%20Values/Region", fakebody) // simulating a put request from a user to update the min temp r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. @@ -519,15 +519,11 @@ func TestHttpSetRegion(t *testing.T) { body, _ := io.ReadAll(resp.Body) // this is a simple check if the JSON response contains the specific value/unit/version value := strings.Contains(string(body), `"value": 1`) - unit := strings.Contains(string(body), `"unit": "RegionPoint"`) version := strings.Contains(string(body), `"version": "SignalA_v1.0"`) if value != true { t.Errorf("expected the statement to be true!") } - if unit != true { - t.Errorf("expected the unit statement to be true!") - } if version != true { t.Errorf("expected the version statement to be true!") } diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 877a9a9..459a40d 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -228,21 +228,21 @@ func initTemplate() components.UnitAsset { Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", } setUserTemp := components.Service{ - Definition: "userTemp", - SubPath: "userTemp", + Definition: "UserTemp", + SubPath: "UserTemp", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the temperature the user wants regardless of prices (using a GET request)", } setRegion := components.Service{ Definition: "Region", SubPath: "Region", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Details: map[string][]string{"Forms": {"SignalA_v1a"}}, Description: "provides the temperature the user wants regardless of prices (using a GET request)", } return &UnitAsset{ //These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", + Name: "Set_Values", Details: map[string][]string{"Location": {"Kitchen"}}, SEKPrice: 1.5, // Example electricity price in SEK per kWh MinPrice: 1.0, // Minimum price allowed @@ -424,7 +424,6 @@ func (ua *UnitAsset) setRegion(f forms.SignalA_v1a) { func (ua *UnitAsset) getRegion() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.Region - f.Unit = "RegionPoint" f.Timestamp = time.Now() return f } diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go index d87b971..74eb019 100644 --- a/Comfortstat/things_test.go +++ b/Comfortstat/things_test.go @@ -266,8 +266,9 @@ func TestInitTemplate(t *testing.T) { //// unnecessary test, but good for practicing name := uasset.GetName() - if name != "Set Values" { - t.Errorf("expected name of the resource is %v, got %v", uasset.Name, name) + expected := "Set_Values" + if name != expected { + t.Errorf("expected name of the resource to be %v, got %v", expected, name) } Services := uasset.GetServices() if Services == nil { From d6337f81f3f5b347c796b8400c9dc4d5a6c934ec Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 15:31:43 +0100 Subject: [PATCH 097/102] Make the influxdb shut up and stop spamming the logs --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 97a837b..251eade 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,6 +91,7 @@ services: DOCKER_INFLUXDB_INIT_PASSWORD: password DOCKER_INFLUXDB_INIT_ORG: organisation DOCKER_INFLUXDB_INIT_BUCKET: bucket + INFLUXD_LOG_LEVEL: warn ports: - 8086:8086 volumes: From 94a0d64361a095e6f6f8313749cbc303ea1a2d1c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 16:12:13 +0100 Subject: [PATCH 098/102] More tiny fixes on json data --- Comfortstat/things.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 459a40d..68993bb 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -42,7 +42,7 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Period time.Duration `json:"samplingPeriod"` + Period time.Duration `json:"SamplingPeriod"` // DesiredTemp float64 `json:"DesiredTemp"` oldDesiredTemp float64 // keep this field private! @@ -51,7 +51,7 @@ type UnitAsset struct { MaxPrice float64 `json:"MaxPrice"` MinTemp float64 `json:"MinTemp"` MaxTemp float64 `json:"MaxTemp"` - UserTemp float64 `json:"userTemp"` + UserTemp float64 `json:"UserTemp"` Region float64 `json:"Region"` // the user can choose from what region the SEKPrice is taken from } @@ -78,12 +78,11 @@ func priceFeedbackLoop() { // start the control loop for { err := getAPIPriceData(url) - if err != nil { return } - select { + select { case <-ticker.C: // blocks the execution until the ticker fires } @@ -451,6 +450,7 @@ func (ua *UnitAsset) processFeedbackLoop() { ua.Region = GlobalRegion // extracts the electricity price depending on the current time and updates globalPrice now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) + log.Println("TIME:", now) for _, i := range data { if i.TimeStart == now { globalPrice.SEKPrice = i.SEKPrice From 5febf8c37e0b412d3bd2280008c5114907647d81 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 17:09:54 +0100 Subject: [PATCH 099/102] Force use local timezone on docker --- docker-compose.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 251eade..18bdb39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,8 @@ services: - SRC=./sregistrar/*.go - PORT=8443 network_mode: "host" + environment: + TZ: Europe/Stockholm volumes: - ./data/registrar:/data @@ -35,6 +37,8 @@ services: depends_on: - registrar network_mode: "host" + environment: + TZ: Europe/Stockholm volumes: - ./data/orchestrator:/data @@ -51,6 +55,8 @@ services: - registrar - orchestrator network_mode: "host" + environment: + TZ: Europe/Stockholm volumes: - ./data/ds18b20:/data @@ -65,6 +71,8 @@ services: - registrar - orchestrator network_mode: "host" + environment: + TZ: Europe/Stockholm volumes: - ./data/comfortstat:/data @@ -80,6 +88,8 @@ services: - orchestrator - ds18b20 network_mode: "host" + environment: + TZ: Europe/Stockholm volumes: - ./data/zigbee:/data @@ -92,6 +102,7 @@ services: DOCKER_INFLUXDB_INIT_ORG: organisation DOCKER_INFLUXDB_INIT_BUCKET: bucket INFLUXD_LOG_LEVEL: warn + TZ: Europe/Stockholm ports: - 8086:8086 volumes: @@ -111,6 +122,8 @@ services: - zigbee - influxdb network_mode: "host" + environment: + TZ: Europe/Stockholm volumes: - ./data/collector:/data From 20e76732d483d4b3e682e42ec31ce2d17a5453ef Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 14 Feb 2025 17:13:35 +0100 Subject: [PATCH 100/102] Fixes *even harder* the docker timezones --- Comfortstat/things.go | 1 - docker-compose.yml | 29 +++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 68993bb..0f244cd 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -450,7 +450,6 @@ func (ua *UnitAsset) processFeedbackLoop() { ua.Region = GlobalRegion // extracts the electricity price depending on the current time and updates globalPrice now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) - log.Println("TIME:", now) for _, i := range data { if i.TimeStart == now { globalPrice.SEKPrice = i.SEKPrice diff --git a/docker-compose.yml b/docker-compose.yml index 18bdb39..e385843 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,10 +22,10 @@ services: - SRC=./sregistrar/*.go - PORT=8443 network_mode: "host" - environment: - TZ: Europe/Stockholm volumes: - ./data/registrar:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro orchestrator: image: orchestrator:0.1.0 @@ -37,10 +37,10 @@ services: depends_on: - registrar network_mode: "host" - environment: - TZ: Europe/Stockholm volumes: - ./data/orchestrator:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro # Build and run business systems @@ -55,10 +55,10 @@ services: - registrar - orchestrator network_mode: "host" - environment: - TZ: Europe/Stockholm volumes: - ./data/ds18b20:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro comfortstat: image: comfortstat:0.2.0 @@ -71,10 +71,10 @@ services: - registrar - orchestrator network_mode: "host" - environment: - TZ: Europe/Stockholm volumes: - ./data/comfortstat:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro zigbee: image: zigbee:0.2.0 @@ -88,10 +88,10 @@ services: - orchestrator - ds18b20 network_mode: "host" - environment: - TZ: Europe/Stockholm volumes: - ./data/zigbee:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro influxdb: image: influxdb:2.7.11-alpine @@ -100,14 +100,15 @@ services: DOCKER_INFLUXDB_INIT_USERNAME: admin DOCKER_INFLUXDB_INIT_PASSWORD: password DOCKER_INFLUXDB_INIT_ORG: organisation - DOCKER_INFLUXDB_INIT_BUCKET: bucket + DOCKER_INFLUXDB_INIT_BUCKET: arrowhead INFLUXD_LOG_LEVEL: warn - TZ: Europe/Stockholm ports: - 8086:8086 volumes: - ./data/influxdb/data:/var/lib/influxdb2 - ./data/influxdb/config:/etc/influxdb2 + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro collector: image: collector:0.1.0 @@ -122,8 +123,8 @@ services: - zigbee - influxdb network_mode: "host" - environment: - TZ: Europe/Stockholm volumes: - ./data/collector:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro From 714e345845d3691531cbec83d89912b5c9d695c9 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 14 Feb 2025 17:49:35 +0100 Subject: [PATCH 101/102] no longer uses index, uses uniqueid to send requests via zigbee gateway instead --- ZigBeeValve/thing.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 1bec700..8ab8268 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -147,22 +147,26 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi return ua, func() { if ua.Model == "ZHAThermostat" { - // Get correct index in list returned by api/sensors to make sure we always change correct device - err := ua.getConnectedUnits("sensors") - if err != nil { - log.Println("Error occurred during startup, while calling getConnectedUnits:", err) - } - err = ua.sendSetPoint() + /* + // Get correct index in list returned by api/sensors to make sure we always change correct device + err := ua.getConnectedUnits("sensors") + if err != nil { + log.Println("Error occurred during startup, while calling getConnectedUnits:", err) + } + */ + err := ua.sendSetPoint() if err != nil { log.Println("Error occurred during startup, while calling sendSetPoint():", err) // TODO: Turn off system if this startup() fails? } } else if ua.Model == "Smart plug" { - // Get correct index in list returned by api/lights to make sure we always change correct device - err := ua.getConnectedUnits("lights") - if err != nil { - log.Println("Error occurred during startup, while calling getConnectedUnits:", err) - } + /* + // Get correct index in list returned by api/lights to make sure we always change correct device + err := ua.getConnectedUnits("lights") + if err != nil { + log.Println("Error occurred during startup, while calling getConnectedUnits:", err) + } + */ // Not all smart plugs should be handled by the feedbackloop, some should be handled by a switch if ua.Period != 0 { // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles @@ -274,7 +278,7 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config // --- Send setpoint to specific unit --- - apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.deviceIndex + "/config" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Uniqueid + "/config" // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload req, err := createRequest(s, apiURL) @@ -286,7 +290,7 @@ func (ua *UnitAsset) sendSetPoint() (err error) { func (ua *UnitAsset) toggleState(state bool) (err error) { // API call to toggle smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config - apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.deviceIndex + "/state" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Uniqueid + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload req, err := createRequest(s, apiURL) From cfe646510edf8467a5dda50e0fa1a4e7b8735013 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 15 Feb 2025 17:12:32 +0100 Subject: [PATCH 102/102] Adds project description and a silly Oxford comma --- .github/workflows/main.yml | 2 +- README.md | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c94273..4bc1691 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Linters, Spellcheck and Tests +name: Linters, Spellcheck, and Tests on: push: diff --git a/README.md b/README.md index d91e7cf..bc1a30a 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,14 @@ "Demand Response with ZigBee and mbaigo." -The public code repo. +**The public code repo.** + +## Description + +This project aims to reduce society's energy consumption by controlling smart power +devices, making adjustments according to the daily power market and the local weather. +Powered by a Internet-of-Things cloud that you can run locally and privately, +using an alternative [Arrowhead] implementation in [Go]. + +[Arrowhead]: https://arrowhead.eu/eclipse-arrowhead-2/ +[Go]: https://github.com/sdoque/mbaigo