From b3481c4830f6cf3ee54f7313f503c5d47cf496f5 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 21 Jan 2025 10:25:30 +0100 Subject: [PATCH 01/58] 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 a6f269d35042a1fb1c7723dce0113ba816da5efe Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 22 Jan 2025 11:22:55 +0100 Subject: [PATCH 02/58] 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 1223c78894bac337d72b470c74e7eea17f098ac8 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Wed, 22 Jan 2025 12:51:45 +0100 Subject: [PATCH 03/58] 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 f5ded85f61b1d943c52588bdbe08fc5f9238ba76 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Thu, 23 Jan 2025 13:42:53 +0100 Subject: [PATCH 04/58] 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 a146df783a291568f162ad71d7d67bbd692b4986 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Thu, 23 Jan 2025 14:00:19 +0100 Subject: [PATCH 05/58] 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 7ca06c153110ef67031c83e5f8ef716b58cd1fd0 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Thu, 23 Jan 2025 16:32:20 +0100 Subject: [PATCH 06/58] Added Alex fix to Jan's code --- go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.mod b/go.mod index 82ef070..f731fe8 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 + +replace github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 => github.com/lmas/mbaigo v0.0.0-20250123014631-ad869265483c From a95b553d97207c21fb14082ab26990dcb62c978f Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Thu, 23 Jan 2025 16:35:46 +0100 Subject: [PATCH 07/58] Ran go mod tidy --- go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.sum b/go.sum index 0f4b1d6..674808d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +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= +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= From cc8ff4394b3a9388059939d95eff6ecde7178b38 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 23 Jan 2025 16:36:29 +0100 Subject: [PATCH 08/58] 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 d6e6a1f36e890b74ae5ffd67d4ea0621862202e7 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 23 Jan 2025 21:01:23 +0100 Subject: [PATCH 09/58] 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 8fb3b25b80b4bc91c16924a342029487ed08b236 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Fri, 24 Jan 2025 10:45:33 +0100 Subject: [PATCH 10/58] 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 6bd7214d7cdb79c8b891ef59c667bcc36c8e67e2 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 24 Jan 2025 13:06:44 +0100 Subject: [PATCH 11/58] 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 8b9f869e3232bce683c656bd0fe3c5db91c737ba Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 24 Jan 2025 14:08:35 +0100 Subject: [PATCH 12/58] 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 260cba2139031d274137505ec6798eadc13f865c Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 24 Jan 2025 16:05:51 +0100 Subject: [PATCH 13/58] 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 ffe9cc166b6f58643b3e49bcdec7a27334d2d8f9 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 28 Jan 2025 09:15:03 +0100 Subject: [PATCH 14/58] 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 95fac3b0f0415c05e1ea0ee9af8d4c4485403849 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 28 Jan 2025 10:51:37 +0100 Subject: [PATCH 15/58] 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 1ea88dc2a18e4770afb1259ba3effbd1c1f643fe Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 28 Jan 2025 11:29:56 +0100 Subject: [PATCH 16/58] 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 db73ae62ec1fefd5ee37813154be811c063dd702 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 11:24:50 +0100 Subject: [PATCH 17/58] 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 d453a4a02152521de89ba0719bedb5bc4451a261 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 11:25:38 +0100 Subject: [PATCH 18/58] 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 39b6aa02485a14c2ecb65812094b2ac78493adea Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 12:04:14 +0100 Subject: [PATCH 19/58] 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 f5fef4b01a507d9704ce298a9f51a082db4b39be Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 29 Jan 2025 16:22:59 +0100 Subject: [PATCH 20/58] 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 2224f3d4e0e25e9908e728b20ac3f98533965862 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 12:48:20 +0100 Subject: [PATCH 21/58] 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 41add31ba5bef4957d311bfc9c1c82bb84cdf272 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 12:50:33 +0100 Subject: [PATCH 22/58] 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 1281db4ef26bf7a294d1cd92109889df7120a09d Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 15:05:43 +0100 Subject: [PATCH 23/58] 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 2cf76b61472e5561ede4c11e6ee66d53f63651a6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Jan 2025 15:18:57 +0100 Subject: [PATCH 24/58] 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 b2dbfe2d360c6609feaba6f6b7529be73d6a6312 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 30 Jan 2025 18:38:01 +0100 Subject: [PATCH 25/58] 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 29b556db6d9b804227ccafc89d06a4e773caaa82 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 31 Jan 2025 17:45:15 +0100 Subject: [PATCH 26/58] 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 2521e46c12cb6554e0d5de1b78efc597c5cbe1d5 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Sun, 2 Feb 2025 13:23:30 +0100 Subject: [PATCH 27/58] 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 161e28726fccd6f5c02e489439a3b24e64e63877 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Mon, 3 Feb 2025 11:30:44 +0100 Subject: [PATCH 28/58] 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 8b7971bce2bc9b04c5791646d24f4f8deab4e243 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Mon, 3 Feb 2025 12:36:12 +0100 Subject: [PATCH 29/58] 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 2434825373701b7403c9c5b48ad718724f31b3be Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Mon, 3 Feb 2025 12:58:51 +0100 Subject: [PATCH 30/58] 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 64b925d58786a7697c14b04ea870dd2e4a99f8aa Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 09:43:14 +0100 Subject: [PATCH 31/58] 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 79cab32cd613c3e9e3ede588309084acf4726021 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 09:57:42 +0100 Subject: [PATCH 32/58] 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 de331da0dea00f96c482b90aeab56c57a5c3217f Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:08:21 +0100 Subject: [PATCH 33/58] 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 9b0086aca255b1fe50b972f3a92fe622c9a51fe5 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:13:30 +0100 Subject: [PATCH 34/58] 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 9a8d9af7c969d8d94abb9a7e8dca253e7df542cf Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:21:56 +0100 Subject: [PATCH 35/58] 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 01af2e19cb493056f6c2427cde9354d861e42a10 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 10:59:56 +0100 Subject: [PATCH 36/58] 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 e4dca4e40cb3d01a9f7c8caebba2e8db01f32ab0 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 11:14:49 +0100 Subject: [PATCH 37/58] 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 4e22141226bcf6473a716d01eed2439fda8ee295 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 11:31:34 +0100 Subject: [PATCH 38/58] 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 7ccffce1c05779e1a62560d28edef1a3716f91a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Feb 2025 11:49:54 +0100 Subject: [PATCH 39/58] 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 108b24552a4cf4d316f217c22357602263940267 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 4 Feb 2025 14:06:15 +0100 Subject: [PATCH 40/58] 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 9308c0a4a83e264dbcc94438519033b78eca8c69 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Wed, 5 Feb 2025 09:57:31 +0100 Subject: [PATCH 41/58] 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 2e32040ff5554e0667e4a6f7fd98c64484ed0c6c Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Fri, 7 Feb 2025 14:15:06 +0100 Subject: [PATCH 42/58] 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 4897efbb78e64b3b47dc5892ebc5cf07d7648f0a Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 7 Feb 2025 15:12:03 +0100 Subject: [PATCH 43/58] 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 27915d773481d330acff9e10118b42c1b55336d8 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 7 Feb 2025 15:52:52 +0100 Subject: [PATCH 44/58] 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 f4667feddca0982bb2d33c8438ad2863beb38c1a Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Mon, 10 Feb 2025 10:39:42 +0100 Subject: [PATCH 45/58] 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 dfb65b578963d987e1d6184f0215c4c9003f5a6c Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Mon, 10 Feb 2025 11:15:06 +0100 Subject: [PATCH 46/58] 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 aed44a4376e26e8efb0a15ccb4fbbadab85c5326 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 11 Feb 2025 08:58:57 +0100 Subject: [PATCH 47/58] 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 e68e0394bbd4a20aeda5241e7d8bc97be66b62b2 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Tue, 11 Feb 2025 10:49:50 +0100 Subject: [PATCH 48/58] 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 ed7d5841b08ad1fbb3e9b74d9d2bfcf82b807bf5 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Tue, 11 Feb 2025 15:14:23 +0100 Subject: [PATCH 49/58] 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 9b7a64bf862ed2860b38c7a2e0d7477a94070a60 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 12 Feb 2025 11:31:26 +0100 Subject: [PATCH 50/58] 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 885c07a4edb7871e730154f27d4e22a66a8ddb50 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Wed, 12 Feb 2025 12:00:26 +0100 Subject: [PATCH 51/58] 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 b59a0fea2258dba723f5f6740e82c8b61e96cc4e Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 13 Feb 2025 10:13:37 +0100 Subject: [PATCH 52/58] 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 9cc83989c9b95189f29cd147c18c2e8c95e902f1 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Thu, 13 Feb 2025 10:14:33 +0100 Subject: [PATCH 53/58] 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 f4630b9a881647e3e428cab048f4c9019bd01180 Mon Sep 17 00:00:00 2001 From: gabaxh-2 Date: Tue, 25 Feb 2025 12:44:34 +0100 Subject: [PATCH 54/58] Added files for the SunButton system --- SunButton/SunButton.go | 135 +++++++++++++++++ SunButton/thing.go | 323 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 458 insertions(+) create mode 100644 SunButton/SunButton.go create mode 100644 SunButton/thing.go diff --git a/SunButton/SunButton.go b/SunButton/SunButton.go new file mode 100644 index 0000000..b0272a7 --- /dev/null +++ b/SunButton/SunButton.go @@ -0,0 +1,135 @@ +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("SunButton", ctx) + + // Instantiate the Capsule + sys.Husk = &components.Husk{ + Description: "Is a controller for a consumed button based on a consumed time of day. Powered by SunriseSunset.io", + 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/SunButton", + } + + // 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 := newUnitAsset(uac, &sys, servsTemp) + startup() + 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 (Crtl+C) signal + fmt.Println("\nShutting 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 then the main routine to end +} + +// Serving handles the resource 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 "ButtonStatus": + t.httpSetButton(w, r) + case "Latitude": + t.httpSetLatitude(w, r) + case "Longitude": + t.httpSetLongitude(w, r) + default: + http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) + } +} + +// All these functions below handles HTTP "PUT" or "GET" requests to modify or retrieve the latitude and longitude and the state of the button +// 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 thing.go with the value witch updates the value in the struct +func (rsc *UnitAsset) httpSetButton(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.setButtonStatus(sig) + case "GET": + signalErr := rsc.getButtonStatus() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} + +func (rsc *UnitAsset) httpSetLatitude(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.setLatitude(sig) + case "GET": + signalErr := rsc.getLatitude() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} + +func (rsc *UnitAsset) httpSetLongitude(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.setLongitude(sig) + case "GET": + signalErr := rsc.getLongitude() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} diff --git a/SunButton/thing.go b/SunButton/thing.go new file mode 100644 index 0000000..ac6c35c --- /dev/null +++ b/SunButton/thing.go @@ -0,0 +1,323 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "time" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" +) + +type SunData struct { + Date string `json:"date"` + Sunrise string `json:"sunrise"` + Sunset string `json:"sunset"` + First_light string `json:"first_light"` + Last_light string `json:"last_light"` + Dawn string `json:"dawn"` + Dusk string `json:"dusk"` + Solar_noon string `json:"solar_noon"` + Golden_hour string `json:"golden_hour"` + Day_length string `json:"day_length"` + Timezone string `json:"timezone"` + Utc_offset float64 `json:"utc_offset"` +} + +type Data struct { + Results SunData `json:"results"` + Status string `json:"status"` +} + +// 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 { + Name string `json:"name"` + Owner *components.System `json:"-"` + Details map[string][]string `json:"details"` + ServicesMap components.Services `json:"-"` + CervicesMap components.Cervices `json:"-"` + + Period time.Duration `json:"samplingPeriod"` + + ButtonStatus float64 `json:"ButtonStatus"` + Latitude float64 `json:"Latitude"` + oldLatitude float64 + Longitude float64 `json:"Longitude"` + oldLongitude float64 + data Data +} + +// 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. +// (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) +func initTemplate() components.UnitAsset { + setLatitude := components.Service{ + Definition: "Latitude", + SubPath: "Latitude", + Details: map[string][]string{"Unit": {"Degrees"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current set latitude (using a GET request)", + } + setLongitude := components.Service{ + Definition: "Longitude", + SubPath: "Longitude", + Details: map[string][]string{"Unit": {"Degrees"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current set longitude (using a GET request)", + } + setButtonStatus := components.Service{ + Definition: "ButtonStatus", + SubPath: "ButtonStatus", + Details: map[string][]string{"Unit": {"bool"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the status of a button (using a GET request)", + } + + return &UnitAsset{ + // These fields should reflect a unique asset (ie, a single sensor with unique ID and location) + Name: "Button", + Details: map[string][]string{"Location": {"Kitchen"}}, + Latitude: 65.584816, // Latitude for the button + Longitude: 22.156704, // Longitude for the button + ButtonStatus: 0.5, // Status for the button (on/off) NOTE: This status is neither on or off as default, this is up for the system to decide. + Period: 15, + data: Data{SunData{}, ""}, + + // Maps the provided services from above + ServicesMap: components.Services{ + setLatitude.SubPath: &setLatitude, + setLongitude.SubPath: &setLongitude, + setButtonStatus.SubPath: &setButtonStatus, + }, + } +} + +//////////////////////////////////////////////////////////////////////////////// + +// 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 the ZigBee, therefore the name with the C + t := &components.Cervice{ + Name: "state", + 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), + Latitude: uac.Latitude, + Longitude: uac.Longitude, + ButtonStatus: uac.ButtonStatus, + Period: uac.Period, + data: uac.data, + CervicesMap: components.Cervices{ + t.Name: t, + }, + } + + var ref components.Service + for _, s := range servs { + if s.Definition == "ButtonStatus" { + ref = s + } + } + + ua.CervicesMap["state"].Details = components.MergeDetails(ua.Details, ref.Details) + + // Returns the loaded unit asset and a function to handle + return ua, func() { + // Start the unit asset(s) + go ua.feedbackLoop(sys.Ctx) + } +} + +// getLatitude is used for reading the current latitude +func (ua *UnitAsset) getLatitude() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.Latitude + f.Unit = "Degrees" + f.Timestamp = time.Now() + return f +} + +// setLatitude is used for updating the current latitude +func (ua *UnitAsset) setLatitude(f forms.SignalA_v1a) { + ua.oldLatitude = ua.Latitude + ua.Latitude = f.Value +} + +// getLongitude is used for reading the current longitude +func (ua *UnitAsset) getLongitude() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.Longitude + f.Unit = "Degrees" + f.Timestamp = time.Now() + return f +} + +// setLongitude is used for updating the current longitude +func (ua *UnitAsset) setLongitude(f forms.SignalA_v1a) { + ua.oldLongitude = ua.Longitude + ua.Longitude = f.Value +} + +// getButtonStatus is used for reading the current button status +func (ua *UnitAsset) getButtonStatus() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.ButtonStatus + f.Unit = "bool" + f.Timestamp = time.Now() + return f +} + +// setButtonStatus is used for updating the current button status +func (ua *UnitAsset) setButtonStatus(f forms.SignalA_v1a) { + ua.ButtonStatus = f.Value +} + +// 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() + case <-ctx.Done(): + return + } + } +} + +// This function sends a new button status to the ZigBee system if needed +func (ua *UnitAsset) processFeedbackLoop() { + date := time.Now().Format("2006-01-02") // Gets the current date in the defined format. + if !((ua.data.Results.Date == date) && ((ua.oldLatitude == ua.Latitude) && (ua.oldLongitude == ua.Longitude))) { // If there is a new day or latitude or longitude is changed new data is downloaded. + log.Printf("Sun API has not been called today for this region, downloading sun data...") + err := ua.getAPIData() + if err != nil { + log.Printf("Cannot get sun API data: %s\n", err) + return + } + } + layout := "15:04:05" + sunrise, _ := time.Parse(layout, ua.data.Results.Sunrise) // Saves the sunrise in the layout format. + sunset, _ := time.Parse(layout, ua.data.Results.Sunset) // Saves the sunset in the layout format. + currentTime, _ := time.Parse(layout, time.Now().Local().Format("15:04:05")) // Saves the current time in the layout format. + if currentTime.After(sunrise) && !(currentTime.After(sunset)) { // This checks if the time is between sunrise or sunset, if it is the switch is supposed to turn off. + if ua.ButtonStatus == 0 { // If the button is already off there is no need to send a state again. + log.Printf("The button is already off") + return + } + ua.ButtonStatus = 0 + err := ua.sendStatus() + if err != nil { + return + } + + } else { // If the time is not between sunrise and sunset the button is supposed to be on. + if ua.ButtonStatus == 1 { // If the button is already on there is no need to send a state again. + log.Printf("The button is already on") + return + } + ua.ButtonStatus = 1 + err := ua.sendStatus() + if err != nil { + return + } + } +} + +func (ua *UnitAsset) sendStatus() error { + // Prepare the form to send + var of forms.SignalA_v1a + of.NewForm() + of.Value = ua.ButtonStatus + of.Unit = ua.CervicesMap["state"].Details["Unit"][0] + of.Timestamp = time.Now() + // Pack the new state form + // Pack() converting the data in "of" into JSON format + op, err := usecases.Pack(&of, "application/json") + if err != nil { + return err + } + // Send the new request + err = usecases.SetState(ua.CervicesMap["state"], ua.Owner, op) + if err != nil { + log.Printf("Cannot update ZigBee state: %s\n", err) + return err + } + return nil +} + +var errStatuscode error = fmt.Errorf("bad status code") + +func (ua *UnitAsset) getAPIData() error { + apiURL := fmt.Sprintf(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, ua.Latitude, ua.Longitude, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + parsedURL, err := url.Parse(apiURL) + if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { + return errors.New("the url is invalid") + } + // End of validating the URL // + res, err := http.Get(parsedURL.String()) + if err != nil { + return err + } + body, err := io.ReadAll(res.Body) // Read the payload into body variable + if err != nil { + return err + } + err = json.Unmarshal(body, &ua.data) + + defer res.Body.Close() + + if res.StatusCode > 299 { + return errStatuscode + } + if err != nil { + return err + } + return nil +} From 6b5e824d59b35c2bfd2ba24c3de7a75f8bc20fa8 Mon Sep 17 00:00:00 2001 From: gabaxh Date: Tue, 25 Feb 2025 18:28:18 +0100 Subject: [PATCH 55/58] Added test files for the SunButton files --- SunButton/SunButton_test.go | 215 +++++++++++++++++++++++++ SunButton/thing_test.go | 304 ++++++++++++++++++++++++++++++++++++ 2 files changed, 519 insertions(+) create mode 100644 SunButton/SunButton_test.go create mode 100644 SunButton/thing_test.go diff --git a/SunButton/SunButton_test.go b/SunButton/SunButton_test.go new file mode 100644 index 0000000..c061cdb --- /dev/null +++ b/SunButton/SunButton_test.go @@ -0,0 +1,215 @@ +package main + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHttpSetButton(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": "bool", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", fakebody) // simulating a put request from a user to update the button status + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + ua.httpSetButton(w, r) + + // save the response 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": "bool", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", fakebody) // simulating a put request from a user to update the button status + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetButton(w, r) + // save the response and read the body + resp = w.Result() + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) + } + + // Good case test: GET + w = httptest.NewRecorder() + r = httptest.NewRequest("GET", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", nil) + ua.httpSetButton(w, r) + // calls the method and extracts the response and save is in resp for the upcoming tests + 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.5`) + unit := strings.Contains(string(body), `"unit": "bool"`) + 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!") + } + 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("123", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", nil) + // calls the method and extracts the response and save is in resp for the upcoming tests + ua.httpSetButton(w, r) + resp = w.Result() + if resp.StatusCode != http.StatusNotFound { + t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) + } +} + +func TestHttpSetLatitude(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": 65.584816, "unit": "Degrees", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/Latitude", fakebody) // simulating a put request from a user to update the latitude + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + ua.httpSetLatitude(w, r) + + // save the response 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": "Degrees", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/Latitude", fakebody) // simulating a put request from a user to update the latitude + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetLatitude(w, r) + // save the response 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://172.30.106.39:8670/SunButton/Button/Latitude", nil) + goodStatusCode = 200 + ua.httpSetLatitude(w, r) + + // save the response 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": 65.584816`) + unit := strings.Contains(string(body), `"unit": "Degrees"`) + 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://172.30.106.39:8670/SunButton/Button/Latitude", nil) + ua.httpSetLatitude(w, r) + resp = w.Result() + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + } +} + +func TestHttpSetLongitude(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": 22.156704, "unit": "Degrees", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r := httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/Longitude", fakebody) // simulating a put request from a user to update the longitude + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + goodStatusCode := 200 + ua.httpSetLongitude(w, r) + + // save the response 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": "Degrees", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/Longitude", 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.httpSetLongitude(w, r) + + // save the response 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://172.30.106.39:8670/SunButton/Button/Longitude", nil) + goodStatusCode = 200 + ua.httpSetLongitude(w, r) + + // save the response 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": 22.156704`) + unit := strings.Contains(string(body), `"unit": "Degrees"`) + 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://172.30.106.39:8670/SunButton/Button/Longitude", nil) + ua.httpSetLongitude(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) + } +} diff --git a/SunButton/thing_test.go b/SunButton/thing_test.go new file mode 100644 index 0000000..6fc4b89 --- /dev/null +++ b/SunButton/thing_test.go @@ -0,0 +1,304 @@ +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 +} + +var sunDataExample string = fmt.Sprintf(`[{ + "results": { + "date": "%d-%02d-%02d", + "sunrise": "08:00:00", + "sunset": "20:00:00", + "first_light": "07:00:00", + "last_light": "21:00:00", + "dawn": "07:30:00", + "dusk": "20:30:00", + "solar_noon": "16:00:00", + "golden_hour": "19:00:00", + "day_length": "12:00:00" + "timezone": "CET", + "utc_offset": "1" + }, + "status": "OK" +}]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + +// 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 = "https://sunrisesunset.io/" + +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) + //retrieveAPIPrice(ua) + ua.getAPIData() + + // 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 TestSetMethods(t *testing.T) { + asset := initTemplate().(*UnitAsset) + + // Simulate the input signals + buttonStatus := forms.SignalA_v1a{ + Value: 0, + } + //call and test ButtonStatus + asset.setButtonStatus(buttonStatus) + if asset.ButtonStatus != 0 { + t.Errorf("expected ButtonStatus to be 0, got %f", asset.ButtonStatus) + } + // Simulate the input signals + latitude := forms.SignalA_v1a{ + Value: 65.584816, + } + // call and test Latitude + asset.setLatitude(latitude) + if asset.Latitude != 65.584816 { + t.Errorf("expected Latitude to be 65.584816, got %f", asset.Latitude) + } + // Simulate the input signals + longitude := forms.SignalA_v1a{ + Value: 22.156704, + } + //call and test MinPrice + asset.setLongitude(longitude) + if asset.Longitude != 22.156704 { + t.Errorf("expected Longitude to be 22.156704, got %f", asset.Longitude) + } +} + +func TestGetMethods(t *testing.T) { + uasset := initTemplate().(*UnitAsset) + + // ButtonStatus + // check if the value from the struct is the acctual value that the func is getting + result1 := uasset.getButtonStatus() + if result1.Value != uasset.ButtonStatus { + t.Errorf("expected Value of the ButtonStatus is to be %v, got %v", uasset.ButtonStatus, result1.Value) + } + //check that the Unit is correct + if result1.Unit != "bool" { + t.Errorf("expected Unit to be 'bool', got %v", result1.Unit) + } + // Latitude + // check if the value from the struct is the acctual value that the func is getting + result2 := uasset.getLatitude() + if result2.Value != uasset.Latitude { + t.Errorf("expected Value of the Latitude is to be %v, got %v", uasset.Latitude, result2.Value) + } + //check that the Unit is correct + if result2.Unit != "Degrees" { + t.Errorf("expected Unit to be 'Degrees', got %v", result2.Unit) + } + // Longitude + // check if the value from the struct is the acctual value that the func is getting + result3 := uasset.getLongitude() + if result3.Value != uasset.Longitude { + t.Errorf("expected Value of the Longitude is to be %v, got %v", uasset.Longitude, result3.Value) + } + //check that the Unit is correct + if result3.Unit != "Degrees" { + t.Errorf("expected Unit to be 'Degrees', got %v", result3.Unit) + } +} + +func TestInitTemplate(t *testing.T) { + uasset := initTemplate().(*UnitAsset) + + //// 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["ButtonStatus"].Definition != "ButtonStatus" { + t.Errorf("expected service definition to be ButtonStatus") + } + if Services["Latitude"].Definition != "Latitude" { + t.Errorf("expected service defenition to be Latitude") + } + if Services["Longitude"].Definition != "Longitude" { + t.Errorf("expected service defenition to be Longitude") + } + //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 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 + // instantiate the System + sys := components.NewSystem("SunButton", ctx) + // Instatiate the Capusle + sys.Husk = &components.Husk{ + Description: " is a controller for a consumed smart plug based on status depending on the sun", + 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/Comfortstat/SunButton", + } + setButtonStatus := components.Service{ + Definition: "ButtonStatus", + SubPath: "ButtonStatus", + Details: map[string][]string{"Unit": {"bool"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current button status (using a GET request)", + } + setLatitude := components.Service{ + Definition: "Latitude", + SubPath: "Latitude", + Details: map[string][]string{"Unit": {"Degrees"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the latitide (using a GET request)", + } + setLongitude := components.Service{ + Definition: "Longitude", + SubPath: "Longitude", + Details: map[string][]string{"Unit": {"Degrees"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the longitude (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: "Button", + Details: map[string][]string{"Location": {"Kitchen"}}, + ButtonStatus: 0.5, + Latitude: 65.584816, + Longitude: 22.156704, + Period: 15, + data: Data{SunData{}, ""}, + + // Maps the provided services from above + ServicesMap: components.Services{ + setButtonStatus.SubPath: &setButtonStatus, + setLatitude.SubPath: &setLatitude, + setLongitude.SubPath: &setLongitude, + }, + } + + ua, _ := newUnitAsset(uac, &sys, nil) + // Calls the method that gets the name of the new UnitAsset + name := ua.GetName() + if name != "Button" { + t.Errorf("expected name to be Button, but got: %v", name) + } +} + +/* +// 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 +} + +// cretas a URL that is broken +var brokenURL string = string([]byte{0x7f}) + + +func TestGetAPIPriceData(t *testing.T) { + sunDataExample = fmt.Sprintf(`[{ + "results": { + "date": "%d-%02d-%02d", + "sunrise": "08:00:00", + "sunset": "20:00:00", + "first_light": "07:00:00", + "last_light": "21:00:00", + "dawn": "07:30:00", + "dusk": "20:30:00", + "solar_noon": "16:00:00", + "golden_hour": "19:00:00", + "day_length": "12:00:00" + "timezone": "CET", + "utc_offset": "1" + }, + "status": "OK" + }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), + ) + // creates a fake response + fakeBody := fmt.Sprintf(sunDataExample) + 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(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, 65.584816, 22.156704, 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) +} +*/ From 1e7fb95b75cd63af34e5777accd5c488a5dec44b Mon Sep 17 00:00:00 2001 From: gabaxh Date: Tue, 25 Feb 2025 18:33:57 +0100 Subject: [PATCH 56/58] Correction in test code --- SunButton/thing_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SunButton/thing_test.go b/SunButton/thing_test.go index 6fc4b89..2b06a12 100644 --- a/SunButton/thing_test.go +++ b/SunButton/thing_test.go @@ -163,7 +163,7 @@ func TestInitTemplate(t *testing.T) { //// unnecessary test, but good for practicing name := uasset.GetName() - if name != "Set Values" { + if name != "Button" { t.Errorf("expected name of the resource is %v, got %v", uasset.Name, name) } Services := uasset.GetServices() From 4cc32821fe4e143fe1cb256315f3ca6160e603dc Mon Sep 17 00:00:00 2001 From: gabaxh Date: Tue, 25 Feb 2025 18:47:25 +0100 Subject: [PATCH 57/58] Fixed the test for ButtonStatus --- SunButton/SunButton_test.go | 62 ++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/SunButton/SunButton_test.go b/SunButton/SunButton_test.go index c061cdb..325ba98 100644 --- a/SunButton/SunButton_test.go +++ b/SunButton/SunButton_test.go @@ -12,41 +12,13 @@ import ( func TestHttpSetButton(t *testing.T) { ua := initTemplate().(*UnitAsset) - //Godd test case: PUT - // creates a fake request body with JSON data + // Good case test: GET w := httptest.NewRecorder() - fakebody := bytes.NewReader([]byte(`{"value": 0, "unit": "bool", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r := httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", fakebody) // simulating a put request from a user to update the button status - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + r := httptest.NewRequest("GET", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", nil) goodStatusCode := 200 ua.httpSetButton(w, r) - - // save the response 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": "bool", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read - r = httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", fakebody) // simulating a put request from a user to update the button status - r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. - ua.httpSetButton(w, r) - // save the response and read the body - resp = w.Result() - if resp.StatusCode == goodStatusCode { - t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) - } - - // Good case test: GET - w = httptest.NewRecorder() - r = httptest.NewRequest("GET", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", nil) - ua.httpSetButton(w, r) // calls the method and extracts the response and save is in resp for the upcoming tests - resp = w.Result() + resp := w.Result() if resp.StatusCode != goodStatusCode { t.Errorf("expected good status code: %v, got %v", goodStatusCode, resp.StatusCode) } @@ -65,6 +37,34 @@ func TestHttpSetButton(t *testing.T) { if version != true { t.Errorf("expected the version statment to be true!") } + + //Godd test case: PUT + // creates a fake request body with JSON data + w = httptest.NewRecorder() + fakebody := bytes.NewReader([]byte(`{"value": 0, "unit": "bool", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", fakebody) // simulating a put request from a user to update the button status + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetButton(w, r) + + // save the response 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": "bool", "version": "SignalA_v1.0"}`)) // converts the Jason data so it can be read + r = httptest.NewRequest("PUT", "http://172.30.106.39:8670/SunButton/Button/ButtonStatus", fakebody) // simulating a put request from a user to update the button status + r.Header.Set("Content-Type", "application/json") // basic setup to prevent the request to be rejected. + ua.httpSetButton(w, r) + // save the response and read the body + resp = w.Result() + if resp.StatusCode == goodStatusCode { + t.Errorf("expected bad status code: %v, got %v", goodStatusCode, resp.StatusCode) + } // Bad test case: default part of code // force the case to hit default statement but alter the method w = httptest.NewRecorder() From 8e3f494784b9d5edca3a41bee11ae1a3323dcfe4 Mon Sep 17 00:00:00 2001 From: Simon Pergel Date: Fri, 28 Feb 2025 10:42:45 +0100 Subject: [PATCH 58/58] added more tests regarding the getApiData --- SunButton/thing.go | 10 ++++--- SunButton/thing_test.go | 58 +++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/SunButton/thing.go b/SunButton/thing.go index ac6c35c..04e9831 100644 --- a/SunButton/thing.go +++ b/SunButton/thing.go @@ -233,10 +233,12 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { // This function sends a new button status to the ZigBee system if needed func (ua *UnitAsset) processFeedbackLoop() { - date := time.Now().Format("2006-01-02") // Gets the current date in the defined format. + date := time.Now().Format("2006-01-02") // Gets the current date in the defined format. + apiURL := fmt.Sprintf(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, ua.Latitude, ua.Longitude, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + if !((ua.data.Results.Date == date) && ((ua.oldLatitude == ua.Latitude) && (ua.oldLongitude == ua.Longitude))) { // If there is a new day or latitude or longitude is changed new data is downloaded. log.Printf("Sun API has not been called today for this region, downloading sun data...") - err := ua.getAPIData() + err := ua.getAPIData(apiURL) if err != nil { log.Printf("Cannot get sun API data: %s\n", err) return @@ -294,8 +296,8 @@ func (ua *UnitAsset) sendStatus() error { var errStatuscode error = fmt.Errorf("bad status code") -func (ua *UnitAsset) getAPIData() error { - apiURL := fmt.Sprintf(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, ua.Latitude, ua.Longitude, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) +func (ua *UnitAsset) getAPIData(apiURL string) error { + //apiURL := fmt.Sprintf(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, ua.Latitude, ua.Longitude, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) parsedURL, err := url.Parse(apiURL) if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" { return errors.New("the url is invalid") diff --git a/SunButton/thing_test.go b/SunButton/thing_test.go index 2b06a12..a22c65a 100644 --- a/SunButton/thing_test.go +++ b/SunButton/thing_test.go @@ -3,6 +3,8 @@ package main import ( "context" "fmt" + "io" + "strings" //"io" "net/http" @@ -73,6 +75,10 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er const apiDomain string = "https://sunrisesunset.io/" func TestSingleUnitAssetOneAPICall(t *testing.T) { + ua := initTemplate().(*UnitAsset) + //maby better to use a getter method + url := fmt.Sprintf(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, ua.Latitude, ua.Longitude, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + resp := &http.Response{ Status: "200 OK", StatusCode: 200, @@ -80,9 +86,10 @@ 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) - ua.getAPIData() + // better to use a getter method?? + ua.getAPIData(url) // TEST CASE: cause a single API request hits := trans.domainHits(apiDomain) @@ -251,7 +258,6 @@ func TestNewUnitAsset(t *testing.T) { } } -/* // Fuctions that help creating bad body type errReader int @@ -268,9 +274,10 @@ func (errReader) Close() error { // cretas a URL that is broken var brokenURL string = string([]byte{0x7f}) - -func TestGetAPIPriceData(t *testing.T) { - sunDataExample = fmt.Sprintf(`[{ +func TestGetAPIPriceDataSun(t *testing.T) { + ua := initTemplate().(*UnitAsset) + // Should not be an array, it should match the exact struct + sunDataExample = fmt.Sprintf(`{ "results": { "date": "%d-%02d-%02d", "sunrise": "08:00:00", @@ -281,12 +288,12 @@ func TestGetAPIPriceData(t *testing.T) { "dusk": "20:30:00", "solar_noon": "16:00:00", "golden_hour": "19:00:00", - "day_length": "12:00:00" + "day_length": "12:00:00", "timezone": "CET", - "utc_offset": "1" + "utc_offset": 1 }, "status": "OK" - }]`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), + }`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), ) // creates a fake response fakeBody := fmt.Sprintf(sunDataExample) @@ -297,8 +304,37 @@ func TestGetAPIPriceData(t *testing.T) { } // Testing good cases // Test case: goal is no errors - url := fmt.Sprintf(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, 65.584816, 22.156704, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + apiURL := fmt.Sprintf(`http://api.sunrisesunset.io/json?lat=%06f&lng=%06f&timezone=CET&date=%d-%02d-%02d&time_format=24`, ua.Latitude, ua.Longitude, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) + fmt.Println("API URL:", apiURL) + // creates a mock HTTP transport to simulate api respone for the test newMockTransport(resp) + err := ua.getAPIData(apiURL) + 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 = ua.getAPIData(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 = ua.getAPIData(apiURL) + 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 = ua.getAPIData(apiURL) + // check the statuscode is bad, witch is expected for the test to be successful + if err != errStatuscode { + t.Errorf("expected an bad status code but got %v", err) + } } -*/