diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d0f615..d95f148 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.23 + - name: Install dependencies + run: make deps - name: Run tests run: make test - name: Report stats diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go index cd2480c..9f4a6ff 100644 --- a/Comfortstat/Comfortstat.go +++ b/Comfortstat/Comfortstat.go @@ -31,6 +31,9 @@ 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() sys.UAssets[assetName] = &assetTemplate @@ -45,8 +48,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 } @@ -66,83 +69,110 @@ 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 { - 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.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) } } -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) } } -// TODO: split up this function to two sepreate function that sets on max and min price. -func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { +// 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) httpSetMinTemp(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + 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) + if err != nil { + //log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return + } + rsc.setMaxTemp(sig) + case "GET": + signalErr := rsc.getMaxTemp() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) + } +} + +func (rsc *UnitAsset) httpSetMinPrice(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return } - rsc.setMax_temp(sig) + rsc.setMinPrice(sig) case "GET": - signalErr := rsc.getMax_temp() + signalErr := rsc.getMinPrice() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) + } } -// LOOK AT: I guess that we probable only need to if there is a PUT from user? -// LOOK AT: so not the GET! -// For PUT - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, call set_minMaxprice with the value -// wich updates the value in thge struct -func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetMaxPrice(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return } - rsc.setMin_price(sig) + rsc.setMaxPrice(sig) case "GET": - signalErr := rsc.getMin_price() + signalErr := rsc.getMaxPrice() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) @@ -150,33 +180,53 @@ 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) httpSetDesiredTemp(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + //log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorreclty formated", http.StatusBadRequest) + return } - rsc.setMax_price(sig) + rsc.setDesiredTemp(sig) case "GET": - signalErr := rsc.getMax_price() + signalErr := rsc.getDesiredTemp() usecases.HTTPProcessGetRequest(w, r, &signalErr) default: http.Error(w, "Method is not supported.", http.StatusNotFound) + } + +} +func (rsc *UnitAsset) httpSetUserTemp(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.setUserTemp(sig) + case "GET": + signalErr := rsc.getUserTemp() + usecases.HTTPProcessGetRequest(w, r, &signalErr) + default: + http.Error(w, "Method is not supported.", http.StatusNotFound) } } -func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { +func (rsc *UnitAsset) httpSetRegion(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting request of the position ", err) + http.Error(w, "request incorrectly formated", http.StatusBadRequest) + return } - rsc.setDesired_temp(sig) + rsc.setRegion(sig) case "GET": - signalErr := rsc.getDesired_temp() + signalErr := rsc.getRegion() 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 new file mode 100644 index 0000000..88dfe49 --- /dev/null +++ b/Comfortstat/Comfortstat_test.go @@ -0,0 +1,545 @@ +package main + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +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/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 != 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 + 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!") + } + if version != true { + t.Errorf("expected the version statment to be true!") + } + // Bad test case: default part of code + w = httptest.NewRecorder() + 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.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 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/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 != 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/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 == 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/MinTemperature", nil) + goodStatusCode = 200 + ua.httpSetMinTemp(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": 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/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 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/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 != 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/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 == 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/MaxTemperature", nil) + goodStatusCode = 200 + ua.httpSetMaxTemp(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": 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 + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "localhost:8670/Comfortstat/Set%20Values/MaxTemperature", nil) + + 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 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/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 != 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", "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() + 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/MinPrice", nil) + goodStatusCode = 200 + ua.httpSetMinPrice(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": "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", "http://localhost:8670/Comfortstat/Set%20Values/MinPrice", nil) + ua.httpSetMinPrice(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) + } +} + +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", "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) + + // 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": "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 + 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/MaxPrice", nil) + goodStatusCode = 200 + ua.httpSetMaxPrice(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": 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 + w = httptest.NewRecorder() + r = httptest.NewRequest("666", "http://localhost:8670/Comfortstat/Set%20Values/MaxPrice", nil) + + ua.httpSetMaxPrice(w, r) + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("expected the status to be bad but got: %v", resp.StatusCode) + } +} + +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", "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) + + // 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/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 + 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/DesiredTemp", nil) + goodStatusCode = 200 + ua.httpSetDesiredTemp(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/DesiredTemp", nil) + // calls the method and extracts the response and save is in resp for the upcoming tests + 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) + } +} + +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/api_fetch_test.go b/Comfortstat/api_fetch_test.go deleted file mode 100644 index 238e1d9..0000000 --- a/Comfortstat/api_fetch_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "strings" - "testing" - "time" -) - -// mockTransport is used for replacing the default network Transport (used by -// http.DefaultClient) and it will intercept network requests. -type mockTransport struct { - hits map[string]int -} - -func newMockTransport() mockTransport { - t := mockTransport{ - hits: make(map[string]int), - } - // Highjack the default http client so no actuall http requests are sent over the network - http.DefaultClient.Transport = t - return t -} - -// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). -func (t mockTransport) domainHits(domain string) int { - for u, hits := range t.hits { - if u == domain { - return hits - } - } - return -1 -} - -// TODO: this might need to be expanded to a full JSON array? -const priceExample string = `[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" -}]` - -// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). -// It prevents the request from being sent over the network and count how many times -// a domain was requested. -func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) - // TODO: should be able to adjust these return values for the error cases - resp = &http.Response{ - Status: "200 OK", - StatusCode: 200, - Request: req, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - t.hits[req.URL.Hostname()] += 1 - return -} - -//////////////////////////////////////////////////////////////////////////////// - -const apiDomain string = "www.elprisetjustnu.se" - -func TestAPIDataFetchPeriod(t *testing.T) { - want := 3600 - if apiFetchPeriod < want { - t.Errorf("expected API fetch period >= %d, got %d", want, apiFetchPeriod) - } -} - -func TestSingleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates a single UnitAsset and assert it only sends a single API request - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - - // TEST CASE: cause a single API request - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests", hits) - } - - // TODO: try more test cases! -} - -func TestMultipleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates multiple UnitAssets and monitor their API requests - units := 10 - for i := 0; i < units; i++ { - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - } - - // TEST CASE: causing only one API hit while using multiple UnitAssets - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests (from %d units)", hits, units) - } - - // TODO: more test cases?? -} diff --git a/Comfortstat/things.go b/Comfortstat/things.go index 711e710..5bd2b42 100644 --- a/Comfortstat/things.go +++ b/Comfortstat/things.go @@ -3,11 +3,12 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "io" "log" - "math" "net/http" + "net/url" "time" "github.com/sdoque/mbaigo/components" @@ -15,11 +16,26 @@ import ( "github.com/sdoque/mbaigo/usecases" ) +type GlobalPriceData struct { + 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{ + 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. // 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 @@ -28,22 +44,121 @@ 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"` + 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"` + 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() } -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"` +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_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) + + if err != nil { + return + } + select { + + case <-ticker.C: + // blocks the execution until the ticker fires + } + } +} + +// This function checks if the user has changed price-region and then calls the getAPIPriceData function which gets the right pricedata +func switchRegion() { + 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 + +// 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 URL, .schema and .Host checks prevent emty or altered URL + 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, &data) // "unpack" body from []byte to []GlobalPriceData, save errors + + defer res.Body.Close() + + if res.StatusCode > 299 { + return errStatuscode + } + if err != nil { + return err + } + return nil } // GetName returns the name of the Resource. @@ -73,67 +188,82 @@ 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", + + 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", // 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?? + 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)", } + 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)", + } + 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{ - // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", - Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 7.5, // Example electricity price in SEK per kWh - Min_price: 0.0, // Minimum price allowed - Max_price: 0.02, // Maximum price allowed - Min_temp: 20.0, // Minimum temperature - Max_temp: 25.0, // Maximum temprature allowed - Desired_temp: 0, // Desired temp calculated by system - Period: 15, - - // Don't forget to map the provided services from above! + //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"}}, + 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, + Region: 1, + + // 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, + setUserTemp.SubPath: &setUserTemp, + setRegion.SubPath: &setRegion, }, } } @@ -148,7 +278,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", @@ -158,17 +288,19 @@ 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, + 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, + Region: uac.Region, CervicesMap: components.Cervices{ t.Name: t, }, @@ -176,198 +308,125 @@ 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 } } ua.CervicesMap["setpoint"].Details = components.MergeDetails(ua.Details, ref.Details) - // ua.CervicesMap["setPoint"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}) - - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - go ua.API_feedbackLoop(sys.Ctx) - - // Optionally start background tasks here! Example: - go func() { - log.Println("Starting up " + ua.Name) - }() - - // Returns the loaded unit asset and an function to handle optional cleanup at shutdown + // Returns the loaded unit asset and an function to handle return ua, func() { - log.Println("Cleaning up " + ua.Name) + // start the unit asset(s) + go ua.feedbackLoop(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 } -// 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) { +// 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 - log.Printf("new minimum price: %.1f", f.Value) - ua.processFeedbackLoop() +// 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 - log.Printf("new maximum price: %.1f", f.Value) - ua.processFeedbackLoop() +// 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 - log.Printf("new minimum temperature: %.1f", f.Value) - ua.processFeedbackLoop() +// 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 - log.Printf("new maximum temperature: %.1f", f.Value) - ua.processFeedbackLoop() +// 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 - log.Printf("new desired temperature: %.1f", f.Value) +func (ua *UnitAsset) setDesiredTemp(f forms.SignalA_v1a) { + ua.DesiredTemp = f.Value } -//TODO: This fuction is used for checking the electric price ones every x hours and so on -//TODO: Needs to be modified to match our needs, not using processFeedbacklopp -//TODO: So mayby the period is every hour, call the api to receive the current price ( could be every 24 hours) -//TODO: This function is may be better in the COMFORTSTAT MAIN - -// It's _strongly_ encouraged to not send requests to the API for more than once per hour. -// Making this period a private constant prevents a user from changing this value -// in the config file. -const apiFetchPeriod int = 3600 - -// feedbackLoop is THE control loop (IPR of the system) -func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) - defer ticker.Stop() - - // start the control loop - for { - retrieveAPI_price(ua) - select { - case <-ticker.C: - // Block the loop until the next period - case <-ctx.Done(): - return - } +func (ua *UnitAsset) setUserTemp(f forms.SignalA_v1a) { + ua.UserTemp = f.Value + if ua.UserTemp != 0 { + ua.sendUserTemp() } } -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) +func (ua *UnitAsset) getUserTemp() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.UserTemp + f.Unit = "Celsius" + f.Timestamp = time.Now() + return f +} +func (ua *UnitAsset) setRegion(f forms.SignalA_v1a) { + ua.Region = f.Value + GlobalRegion = ua.Region + switchRegion() +} - // Don't send temperature updates if the difference is too low - // (this could potentially save on battery!) - new_temp := ua.calculateDesiredTemp() - if math.Abs(ua.Desired_temp-new_temp) < 0.5 { - return - } - ua.Desired_temp = new_temp +func (ua *UnitAsset) getRegion() (f forms.SignalA_v1a) { + f.NewForm() + f.Value = ua.Region + f.Unit = "RegionPoint" + f.Timestamp = time.Now() + return f } // feedbackLoop is THE control loop (IPR of the system) @@ -387,49 +446,42 @@ 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 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 + 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 { + if i.TimeStart == now { + globalPrice.SEKPrice = i.SEKPrice } - // 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") + } + + ua.SEKPrice = globalPrice.SEKPrice + + //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.DesiredTemp == ua.oldDesiredTemp) || (ua.UserTemp != 0) { + if ua.UserTemp != 0 { + ua.oldDesiredTemp = ua.UserTemp return } - */ - /* - miT := ua.getMin_temp().Value - maT := ua.getMax_temp().Value - miP := ua.getMin_price().Value - maP := ua.getMax_price().Value - */ - //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) - // Only send temperature update when we have a new value. - if ua.Desired_temp == ua.old_desired_temp { return } // Keep track of previous value - ua.old_desired_temp = ua.Desired_temp - - // perform the control algorithm - // ua.deviation = ua.Setpt - tup.Value - // output := ua.calculateOutput(ua.deviation) + 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() // 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 @@ -442,17 +494,38 @@ 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 + + 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.Max_temp - ua.Min_temp) / (ua.Max_price - ua.Min_price) - //m := max_temp - (k * min_price) - //m := max_temp - desired_temp := k*(ua.SEK_price-ua.Min_price) + ua.Min_temp // y - y_min = k*(x-x_min), solve for y ("desired temp") - return desired_temp + k := (ua.MinTemp - ua.MaxTemp) / (ua.MaxPrice - ua.MinPrice) + m := ua.MaxTemp - (k * ua.MinPrice) + DesiredTemp := k*(ua.SEKPrice) + m + + return DesiredTemp +} + +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() + + 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 + } } diff --git a/Comfortstat/things_test.go b/Comfortstat/things_test.go new file mode 100644 index 0000000..640d90a --- /dev/null +++ b/Comfortstat/things_test.go @@ -0,0 +1,500 @@ +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) + //retrieveAPIPrice(ua) + ua.getSEKPrice() + + // 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) + //retrieveAPIPrice(ua) + ua.getSEKPrice() + } + // 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 + MinTempInputSignal := forms.SignalA_v1a{ + Value: 1.0, + } + //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) { + uasset := initTemplate().(*UnitAsset) + + ////MinTemp//// + // check if the value from the struct is the acctual value that the func is getting + result := uasset.getMinTemp() + if result.Value != uasset.MinTemp { + t.Errorf("expected Value of the MinTemp is to be %v, got %v", uasset.MinTemp, result.Value) + } + //check that the Unit is correct + if result.Unit != "Celsius" { + t.Errorf("expected Unit to be 'Celsius', got %v", result.Unit) + } + ////MaxTemp//// + 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 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.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" { + 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.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" { + 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.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) + } + ////SEKPrice//// + result6 := uasset.getSEKPrice() + 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) { + 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["SEKPrice"].Definition != "SEKPrice" { + t.Errorf("expected service defenition to be SEKprice") + } + if Services["MaxTemperature"].Definition != "MaxTemperature" { + t.Errorf("expected service defenition to be MaxTemperature") + } + if Services["MinTemperature"].Definition != "MinTemperature" { + t.Errorf("expected service defenition to be MinTemperature") + } + if Services["MaxPrice"].Definition != "MaxPrice" { + t.Errorf("expected service defenition to be MaxPrice") + } + if Services["MinPrice"].Definition != "MinPrice" { + t.Errorf("expected service defenition to be MinPrice") + } + if Services["DesiredTemp"].Definition != "DesiredTemp" { + t.Errorf("expected service defenition to be DesiredTemp") + } + //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("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", + } + 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)", + } + 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)", + } + 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)", + } + 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)", + } + 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)", + } + 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"}}, + 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{ + setMaxTemp.SubPath: &setMaxTemp, + setMinTemp.SubPath: &setMinTemp, + setMaxPrice.SubPath: &setMaxPrice, + setMinPrice.SubPath: &setMinPrice, + setSEKPrice.SubPath: &setSEKPrice, + setDesiredTemp.SubPath: &setDesiredTemp, + }, + } + + 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 TestCalculateDesiredTemp(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 TestSpecialCalculate(t *testing.T) { + asset := UnitAsset{ + SEKPrice: 3.0, + MaxPrice: 2.0, + MinTemp: 17.0, + } + //call the method and save the result in a varable for testing + result := asset.calculateDesiredTemp() + //check the result from the call above + if result != asset.MinTemp { + t.Errorf("Expected temperature to be %v, got %v", asset.MinTemp, 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 +} + +// 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, + "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(), + ) + // 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.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) + // 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) + // 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) + } + // 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) + } +} 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 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=