From 2e3dbe9e6f718989723a7137e8fd92b0e965739e Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 24 Jan 2025 09:35:13 +0100 Subject: [PATCH 01/25] added return statements to errors --- ZigBeeValve/thing.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index d62ef63..c7e88d5 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -198,19 +198,23 @@ func findGateway(ua *UnitAsset) { res, err := http.Get("https://phoscon.de/discover") if err != nil { log.Println("Couldn't get gateway, error:", err) + return } defer res.Body.Close() if res.StatusCode > 299 { log.Printf("Response failed with status code: %d and\n", res.StatusCode) + return } body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { log.Println("Something went wrong while reading the body during discovery, error:", err) + return } var gw []discoverJSON // Create a list to hold the gateway json err = json.Unmarshal(body, &gw) // "unpack" body from []byte to []discoverJSON, save errors if err != nil { log.Println("Error during Unmarshal, error:", err) + return } if len(gw) < 1 { log.Println("No gateway was found") @@ -282,8 +286,10 @@ func sendRequest(data []byte, apiURL string) { b, err := io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { log.Println("Something went wrong while reading the body during discovery, error:", err) + return } if resp.StatusCode > 299 { log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) + return } } From 5aea0b999a62785611eba3132fcd643a0a440137 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 24 Jan 2025 09:38:25 +0100 Subject: [PATCH 02/25] Added testfile --- ZigBeeValve/zigbee_test.go | 100 +++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 ZigBeeValve/zigbee_test.go diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go new file mode 100644 index 0000000..0706247 --- /dev/null +++ b/ZigBeeValve/zigbee_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/sdoque/mbaigo/forms" +) + +// mockTransport is used for replacing the default network Transport (used by +// http.DefaultClient) and it will intercept network requests. + +type mockTransport struct { + hits map[string]int +} + +func newMockTransport() mockTransport { + t := mockTransport{ + hits: make(map[string]int), + } + // Highjack the default http client so no actuall http requests are sent over the network + http.DefaultClient.Transport = t + return t +} + +// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). + +func (t mockTransport) domainHits(domain string) int { + for u, hits := range t.hits { + if u == domain { + return hits + } + } + return -1 +} + +// TODO: this might need to be expanded to a full JSON array? + +const priceExample string = `[{ + "SEK_per_kWh": 0.26673, + "EUR_per_kWh": 0.02328, + "EXR": 11.457574, + "time_start": "2025-01-06T%02d:00:00+01:00", + "time_end": "2025-01-06T%02d:00:00+01:00" +}]` + +// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). +// It prevents the request from being sent over the network and count how many times +// a domain was requested. + +func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + hour := time.Now().Local().Hour() + fakeBody := fmt.Sprintf(priceExample, hour, hour+1) + // TODO: should be able to adjust these return values for the error cases + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Request: req, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + t.hits[req.URL.Hostname()] += 1 + return +} + +//////////////////////////////////////////////////////////////////////////////// + +const thermostatDomain string = "http://localhost:port/api/apikey/sensors/thermostat_index/config" +const plugDomain string = "http://localhost:port/api/apikey/lights/plug_index/config" + +func TestUnitAssetChanged(t *testing.T) { + + // Don't understand how to check my own deConz API calls, will extend the test with this once i understand + trans := newMockTransport() + + // Create a form + f := forms.SignalA_v1a{ + Value: 27.0, + } + + // Creates a single UnitAsset and assert it changes + ua := UnitAsset{ + Setpt: 20.0, + } + + ua.setSetPoint(f) + + if ua.Setpt != 27.0 { + t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) + } + + // TODO: Add api call to make sure it only sends update to HW once. + hits := trans.domainHits(thermostatDomain) + if hits > 1 { + t.Errorf("Expected number of api requests = 1, got %d requests", hits) + } +} From c45cedfad93e06dfd1f3a135188e88d2a2641c82 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 24 Jan 2025 15:25:39 +0100 Subject: [PATCH 03/25] added some tests --- ZigBeeValve/thing.go | 2 +- ZigBeeValve/zigbee_test.go | 59 ++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index c7e88d5..1f352fe 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -82,7 +82,7 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "2", + Name: "Template", Details: map[string][]string{"Location": {"Kitchen"}}, Model: "", Period: 10, diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 0706247..009246d 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "net/http" @@ -8,6 +9,7 @@ import ( "testing" "time" + "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" ) @@ -68,10 +70,10 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er //////////////////////////////////////////////////////////////////////////////// -const thermostatDomain string = "http://localhost:port/api/apikey/sensors/thermostat_index/config" -const plugDomain string = "http://localhost:port/api/apikey/lights/plug_index/config" +const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" +const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" -func TestUnitAssetChanged(t *testing.T) { +func TestUnitAsset(t *testing.T) { // Don't understand how to check my own deConz API calls, will extend the test with this once i understand trans := newMockTransport() @@ -82,10 +84,9 @@ func TestUnitAssetChanged(t *testing.T) { } // Creates a single UnitAsset and assert it changes - ua := UnitAsset{ - Setpt: 20.0, - } + ua := initTemplate().(*UnitAsset) + // Change Setpt ua.setSetPoint(f) if ua.Setpt != 27.0 { @@ -98,3 +99,49 @@ func TestUnitAssetChanged(t *testing.T) { t.Errorf("Expected number of api requests = 1, got %d requests", hits) } } + +func TestGetters(t *testing.T) { + ua := initTemplate().(*UnitAsset) + + name := ua.GetName() + if name != "Template" { + t.Errorf("Expected name to be 2, instead got %s", name) + } + + services := ua.GetServices() + if services == nil { + t.Fatalf("Expected services not to be nil") + } + if services["setpoint"].Definition != "setpoint" { + t.Errorf("Expected definition to be setpoint") + } + + details := ua.GetDetails() + if details == nil { + t.Fatalf("Details was nil, expected map") + } + if len(details["Location"]) == 0 { + t.Fatalf("Location was nil, expected kitchen") + } + + if details["Location"][0] != "Kitchen" { + t.Errorf("Expected location to be Kitchen") + } + + cervices := ua.GetCervices() + if cervices != nil { + t.Errorf("Expected no cervices") + } +} + +func TestNewResource(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var uac UnitAsset + sys := components.NewSystem("testsys", ctx) + servsTemp := []components.Service{} + + ua, cleanup := newResource(uac, &sys, servsTemp) + +} From 81fa3ec23121559e9475a5f8c5ece4f0ae6ad573 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 02:05:31 +0100 Subject: [PATCH 04/25] removed comfortstat from zigbee branch --- Comfortstat/Comfortstat.go | 184 -------------- Comfortstat/api_fetch_test.go | 105 -------- Comfortstat/things.go | 458 ---------------------------------- ZigBeeValve/ZigBeeValve.go | 7 +- ZigBeeValve/thing.go | 26 +- ZigBeeValve/zigbee_test.go | 32 ++- 6 files changed, 46 insertions(+), 766 deletions(-) delete mode 100644 Comfortstat/Comfortstat.go delete mode 100644 Comfortstat/api_fetch_test.go delete mode 100644 Comfortstat/things.go diff --git a/Comfortstat/Comfortstat.go b/Comfortstat/Comfortstat.go deleted file mode 100644 index cd2480c..0000000 --- a/Comfortstat/Comfortstat.go +++ /dev/null @@ -1,184 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/usecases" -) - -func main() { - // prepare for graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) // create a context that can be cancelled - defer cancel() // make sure all paths cancel the context to avoid context leak - - // instantiate the System - sys := components.NewSystem("Comfortstat", ctx) - - // Instatiate the Capusle - sys.Husk = &components.Husk{ - Description: " is a controller for a consumed servo motor position based on a consumed temperature", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8670, "coap": 0}, - InfoLink: "https://github.com/lmas/d0020e_code/tree/master/Comfortstat", - } - - // instantiate a template unit asset - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate - - // Configure the system - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, cleanup := newUnitAsset(uac, &sys, servsTemp) - defer cleanup() - sys.UAssets[ua.GetName()] = &ua - } - - // Generate PKI keys and CSR to obtain a authentication certificate from the CA - usecases.RequestCertificate(&sys) - - // Register the (system) and its services - usecases.RegisterServices(&sys) - - // start the http handler and server - go usecases.SetoutServers(&sys) - - // wait for shutdown signal, and gracefully close properly goroutines with context - <-sys.Sigs // wait for a SIGINT (Ctrl+C) signal - fmt.Println("\nshuting down system", sys.Name) - cancel() // cancel the context, signaling the goroutines to stop - time.Sleep(2 * time.Second) // allow the go routines to be executed, which might take more time than the main routine to end -} - -// TODO: change the namne, will get one function for each of the four cases -// Serving handles the resources services. NOTE: it exepcts those names from the request URL path -func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath string) { - switch servicePath { - case "min_temperature": - t.set_minTemp(w, r) - case "max_temperature": - t.set_maxTemp(w, r) - case "max_price": - t.set_maxPrice(w, r) - case "min_price": - t.set_minPrice(w, r) - case "SEK_price": - t.set_SEKprice(w, r) - case "desired_temp": - t.set_desiredTemp(w, r) - default: - http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) - } -} - -func (rsc *UnitAsset) set_SEKprice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "GET": - signalErr := rsc.getSEK_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} - -// TODO: split up this function to two sepreate function that sets on max and min price. -func (rsc *UnitAsset) set_minTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMin_temp(sig) - case "GET": - signalErr := rsc.getMin_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} -func (rsc *UnitAsset) set_maxTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMax_temp(sig) - case "GET": - signalErr := rsc.getMax_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} - -// LOOK AT: I guess that we probable only need to if there is a PUT from user? -// LOOK AT: so not the GET! -// For PUT - the "HTTPProcessSetRequest(w, r)" is called to prosses the data given from the user and if no error, call set_minMaxprice with the value -// wich updates the value in thge struct -func (rsc *UnitAsset) set_minPrice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMin_price(sig) - case "GET": - signalErr := rsc.getMin_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - - } -} - -func (rsc *UnitAsset) set_maxPrice(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setMax_price(sig) - case "GET": - signalErr := rsc.getMax_price() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - - } -} - -func (rsc *UnitAsset) set_desiredTemp(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PUT": - sig, err := usecases.HTTPProcessSetRequest(w, r) - if err != nil { - log.Println("Error with the setting request of the position ", err) - } - rsc.setDesired_temp(sig) - case "GET": - signalErr := rsc.getDesired_temp() - usecases.HTTPProcessGetRequest(w, r, &signalErr) - default: - http.Error(w, "Method is not supported.", http.StatusNotFound) - } -} diff --git a/Comfortstat/api_fetch_test.go b/Comfortstat/api_fetch_test.go deleted file mode 100644 index 238e1d9..0000000 --- a/Comfortstat/api_fetch_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "strings" - "testing" - "time" -) - -// mockTransport is used for replacing the default network Transport (used by -// http.DefaultClient) and it will intercept network requests. -type mockTransport struct { - hits map[string]int -} - -func newMockTransport() mockTransport { - t := mockTransport{ - hits: make(map[string]int), - } - // Highjack the default http client so no actuall http requests are sent over the network - http.DefaultClient.Transport = t - return t -} - -// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). -func (t mockTransport) domainHits(domain string) int { - for u, hits := range t.hits { - if u == domain { - return hits - } - } - return -1 -} - -// TODO: this might need to be expanded to a full JSON array? -const priceExample string = `[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" -}]` - -// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). -// It prevents the request from being sent over the network and count how many times -// a domain was requested. -func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) - // TODO: should be able to adjust these return values for the error cases - resp = &http.Response{ - Status: "200 OK", - StatusCode: 200, - Request: req, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - t.hits[req.URL.Hostname()] += 1 - return -} - -//////////////////////////////////////////////////////////////////////////////// - -const apiDomain string = "www.elprisetjustnu.se" - -func TestAPIDataFetchPeriod(t *testing.T) { - want := 3600 - if apiFetchPeriod < want { - t.Errorf("expected API fetch period >= %d, got %d", want, apiFetchPeriod) - } -} - -func TestSingleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates a single UnitAsset and assert it only sends a single API request - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - - // TEST CASE: cause a single API request - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests", hits) - } - - // TODO: try more test cases! -} - -func TestMultipleUnitAssetOneAPICall(t *testing.T) { - trans := newMockTransport() - // Creates multiple UnitAssets and monitor their API requests - units := 10 - for i := 0; i < units; i++ { - ua := initTemplate().(*UnitAsset) - retrieveAPI_price(ua) - } - - // TEST CASE: causing only one API hit while using multiple UnitAssets - hits := trans.domainHits(apiDomain) - if hits > 1 { - t.Errorf("expected number of api requests = 1, got %d requests (from %d units)", hits, units) - } - - // TODO: more test cases?? -} diff --git a/Comfortstat/things.go b/Comfortstat/things.go deleted file mode 100644 index 711e710..0000000 --- a/Comfortstat/things.go +++ /dev/null @@ -1,458 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log" - "math" - "net/http" - "time" - - "github.com/sdoque/mbaigo/components" - "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" -) - -// A UnitAsset models an interface or API for a smaller part of a whole system, for example a single temperature sensor. -// This type must implement the go interface of "components.UnitAsset" -type UnitAsset struct { - // Public fields - // TODO: Why have these public and then provide getter methods? Might need refactor.. - Name string `json:"name"` // Must be a unique name, ie. a sensor ID - Owner *components.System `json:"-"` // The parent system this UA is part of - Details map[string][]string `json:"details"` // Metadata or details about this UA - ServicesMap components.Services `json:"-"` - CervicesMap components.Cervices `json:"-"` - // - Period time.Duration `json:"samplingPeriod"` - // - Daily_prices []API_data `json:"-"` - Desired_temp float64 `json:"desired_temp"` - old_desired_temp float64 // keep this field private! - SEK_price float64 `json:"SEK_per_kWh"` - Min_price float64 `json:"min_price"` - Max_price float64 `json:"max_price"` - Min_temp float64 `json:"min_temp"` - Max_temp float64 `json:"max_temp"` -} - -type API_data struct { - SEK_price float64 `json:"SEK_per_kWh"` - EUR_price float64 `json:"EUR_per_kWh"` - EXR float64 `json:"EXR"` - Time_start string `json:"time_start"` - Time_end string `json:"time_end"` -} - -// GetName returns the name of the Resource. -func (ua *UnitAsset) GetName() string { - return ua.Name -} - -// GetServices returns the services of the Resource. -func (ua *UnitAsset) GetServices() components.Services { - return ua.ServicesMap -} - -// GetCervices returns the list of consumed services by the Resource. -func (ua *UnitAsset) GetCervices() components.Cervices { - return ua.CervicesMap -} - -// GetDetails returns the details of the Resource. -func (ua *UnitAsset) GetDetails() map[string][]string { - return ua.Details -} - -// ensure UnitAsset implements components.UnitAsset (this check is done at during the compilation) -var _ components.UnitAsset = (*UnitAsset)(nil) - -//////////////////////////////////////////////////////////////////////////////// - -// initTemplate initializes a new UA and prefils it with some default values. -// The returned instance is used for generating the configuration file, whenever it's missing. -func initTemplate() components.UnitAsset { - // First predefine any exposed services - // (see https://github.com/sdoque/mbaigo/blob/main/components/service.go for documentation) - setSEK_price := components.Service{ - Definition: "SEK_price", - SubPath: "SEK_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the current electric hourly price (using a GET request)", - } - - setMax_temp := components.Service{ - Definition: "max_temperature", // TODO: this get's incorrectly linked to the below subpath - SubPath: "max_temperature", // TODO: this path needs to be setup in Serving() too - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, // TODO: why this form here?? - Description: "provides the maximum temp the user wants (using a GET request)", - } - setMin_temp := components.Service{ - Definition: "min_temperature", - SubPath: "min_temperature", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum temp the user could tolerate (using a GET request)", - } - setMax_price := components.Service{ - Definition: "max_price", - SubPath: "max_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the maximum price the user wants to pay (using a GET request)", - } - setMin_price := components.Service{ - Definition: "min_price", - SubPath: "min_price", - Details: map[string][]string{"Unit": {"SEK"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the minimum price the user wants to pay (using a GET request)", - } - setDesired_temp := components.Service{ - Definition: "desired_temp", - SubPath: "desired_temp", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the desired temperature the system calculates based on user inputs (using a GET request)", - } - - return &UnitAsset{ - // TODO: These fields should reflect a unique asset (ie, a single sensor with unique ID and location) - Name: "Set Values", - Details: map[string][]string{"Location": {"Kitchen"}}, - SEK_price: 7.5, // Example electricity price in SEK per kWh - Min_price: 0.0, // Minimum price allowed - Max_price: 0.02, // Maximum price allowed - Min_temp: 20.0, // Minimum temperature - Max_temp: 25.0, // Maximum temprature allowed - Desired_temp: 0, // Desired temp calculated by system - Period: 15, - - // Don't forget to map the provided services from above! - ServicesMap: components.Services{ - setMax_temp.SubPath: &setMax_temp, - setMin_temp.SubPath: &setMin_temp, - setMax_price.SubPath: &setMax_price, - setMin_price.SubPath: &setMin_price, - setSEK_price.SubPath: &setSEK_price, - setDesired_temp.SubPath: &setDesired_temp, - }, - } -} - -//////////////////////////////////////////////////////////////////////////////// - -// newUnitAsset creates a new and proper instance of UnitAsset, using settings and -// values loaded from an existing configuration file. -// This function returns an UA instance that is ready to be published and used, -// aswell as a function that can perform any cleanup when the system is shutting down. -func newUnitAsset(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { - - sProtocol := components.SProtocols(sys.Husk.ProtoPort) - - // the Cervice that is to be consumed by zigbee, there fore the name with the C - - t := &components.Cervice{ - Name: "setpoint", - Protos: sProtocol, - Url: make([]string, 0), - } - - ua := &UnitAsset{ - // Filling in public fields using the given data - Name: uac.Name, - Owner: sys, - Details: uac.Details, - ServicesMap: components.CloneServices(servs), - SEK_price: uac.SEK_price, - Min_price: uac.Min_price, - Max_price: uac.Max_price, - Min_temp: uac.Min_temp, - Max_temp: uac.Max_temp, - Desired_temp: uac.Desired_temp, - Period: uac.Period, - CervicesMap: components.Cervices{ - t.Name: t, - }, - } - - var ref components.Service - for _, s := range servs { - if s.Definition == "desired_temp" { - ref = s - } - } - - ua.CervicesMap["setpoint"].Details = components.MergeDetails(ua.Details, ref.Details) - - // ua.CervicesMap["setPoint"].Details = components.MergeDetails(ua.Details, map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}) - - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - go ua.API_feedbackLoop(sys.Ctx) - - // Optionally start background tasks here! Example: - go func() { - log.Println("Starting up " + ua.Name) - }() - - // Returns the loaded unit asset and an function to handle optional cleanup at shutdown - return ua, func() { - log.Println("Cleaning up " + ua.Name) - } -} - -// getSEK_price is used for reading the current hourly electric price -func (ua *UnitAsset) getSEK_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.SEK_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setSEK_price updates the current electric price with the new current electric hourly price -func (ua *UnitAsset) setSEK_price(f forms.SignalA_v1a) { - ua.SEK_price = f.Value - log.Printf("new electric price: %.1f", f.Value) -} - -///////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////// - -// getMin_price is used for reading the current value of Min_price -func (ua *UnitAsset) getMin_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Min_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setMin_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMin_price(f forms.SignalA_v1a) { - ua.Min_price = f.Value - log.Printf("new minimum price: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMax_price is used for reading the current value of Max_price -func (ua *UnitAsset) getMax_price() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Max_price - f.Unit = "SEK" - f.Timestamp = time.Now() - return f -} - -// setMax_price updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_price(f forms.SignalA_v1a) { - ua.Max_price = f.Value - log.Printf("new maximum price: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMin_temp is used for reading the current minimum temerature value -func (ua *UnitAsset) getMin_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Min_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -// setMin_temp updates the current minimum temperature set by the user with a new value -func (ua *UnitAsset) setMin_temp(f forms.SignalA_v1a) { - ua.Min_temp = f.Value - log.Printf("new minimum temperature: %.1f", f.Value) - ua.processFeedbackLoop() -} - -// getMax_temp is used for reading the current value of Min_price -func (ua *UnitAsset) getMax_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Max_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -// setMax_temp updates the current minimum price set by the user with a new value -func (ua *UnitAsset) setMax_temp(f forms.SignalA_v1a) { - ua.Max_temp = f.Value - log.Printf("new maximum temperature: %.1f", f.Value) - ua.processFeedbackLoop() -} - -func (ua *UnitAsset) getDesired_temp() (f forms.SignalA_v1a) { - f.NewForm() - f.Value = ua.Desired_temp - f.Unit = "Celsius" - f.Timestamp = time.Now() - return f -} - -func (ua *UnitAsset) setDesired_temp(f forms.SignalA_v1a) { - ua.Desired_temp = f.Value - log.Printf("new desired temperature: %.1f", f.Value) -} - -//TODO: This fuction is used for checking the electric price ones every x hours and so on -//TODO: Needs to be modified to match our needs, not using processFeedbacklopp -//TODO: So mayby the period is every hour, call the api to receive the current price ( could be every 24 hours) -//TODO: This function is may be better in the COMFORTSTAT MAIN - -// It's _strongly_ encouraged to not send requests to the API for more than once per hour. -// Making this period a private constant prevents a user from changing this value -// in the config file. -const apiFetchPeriod int = 3600 - -// feedbackLoop is THE control loop (IPR of the system) -func (ua *UnitAsset) API_feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(time.Duration(apiFetchPeriod) * time.Second) - defer ticker.Stop() - - // start the control loop - for { - retrieveAPI_price(ua) - select { - case <-ticker.C: - // Block the loop until the next period - case <-ctx.Done(): - return - } - } -} - -func retrieveAPI_price(ua *UnitAsset) { - url := fmt.Sprintf(`https://www.elprisetjustnu.se/api/v1/prices/%d/%02d-%02d_SE1.json`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day()) - log.Println("URL:", url) - - res, err := http.Get(url) - if err != nil { - log.Println("Couldn't get the url, error:", err) - return - } - body, err := io.ReadAll(res.Body) // Read the payload into body variable - if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) - return - } - var data []API_data // Create a list to hold the gateway json - err = json.Unmarshal(body, &data) // "unpack" body from []byte to []discoverJSON, save errors - res.Body.Close() // defer res.Body.Close() - - if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) - return - } - if err != nil { - log.Println("Error during Unmarshal, error:", err) - return - } - - ///////// - now := fmt.Sprintf(`%d-%02d-%02dT%02d:00:00+01:00`, time.Now().Local().Year(), int(time.Now().Local().Month()), time.Now().Local().Day(), time.Now().Local().Hour()) - for _, i := range data { - if i.Time_start == now { - ua.SEK_price = i.SEK_price - log.Println("Price in loop is:", i.SEK_price) - } - - } - log.Println("current el-pris is:", ua.SEK_price) - - // Don't send temperature updates if the difference is too low - // (this could potentially save on battery!) - new_temp := ua.calculateDesiredTemp() - if math.Abs(ua.Desired_temp-new_temp) < 0.5 { - return - } - ua.Desired_temp = new_temp -} - -// feedbackLoop is THE control loop (IPR of the system) -func (ua *UnitAsset) feedbackLoop(ctx context.Context) { - // Initialize a ticker for periodic execution - ticker := time.NewTicker(ua.Period * time.Second) - defer ticker.Stop() - - // start the control loop - for { - select { - case <-ticker.C: - ua.processFeedbackLoop() // either modifiy processFeedback loop or write a new one - case <-ctx.Done(): - return - } - } -} - -// - -func (ua *UnitAsset) processFeedbackLoop() { - // get the current temperature - /* - tf, err := usecases.GetState(ua.CervicesMap["setpoint"], ua.Owner) - if err != nil { - log.Printf("\n unable to obtain a setpoint reading error: %s\n", err) - return - } - // Perform a type assertion to convert the returned Form to SignalA_v1a - tup, ok := tf.(*forms.SignalA_v1a) - if !ok { - log.Println("problem unpacking the setpoint signal form") - return - } - */ - /* - miT := ua.getMin_temp().Value - maT := ua.getMax_temp().Value - miP := ua.getMin_price().Value - maP := ua.getMax_price().Value - */ - //ua.Desired_temp = ua.calculateDesiredTemp(miT, maT, miP, maP, ua.getSEK_price().Value) - // Only send temperature update when we have a new value. - if ua.Desired_temp == ua.old_desired_temp { - return - } - // Keep track of previous value - ua.old_desired_temp = ua.Desired_temp - - // perform the control algorithm - // ua.deviation = ua.Setpt - tup.Value - // output := ua.calculateOutput(ua.deviation) - - // prepare the form to send - var of forms.SignalA_v1a - of.NewForm() - of.Value = ua.Desired_temp - of.Unit = ua.CervicesMap["setpoint"].Details["Unit"][0] - of.Timestamp = time.Now() - - // pack the new valve state form - op, err := usecases.Pack(&of, "application/json") - if err != nil { - return - } - // send the new valve state request - err = usecases.SetState(ua.CervicesMap["setpoint"], ua.Owner, op) - if err != nil { - log.Printf("cannot update zigbee setpoint: %s\n", err) - return - } -} - -func (ua *UnitAsset) calculateDesiredTemp() float64 { - if ua.SEK_price <= ua.Min_price { - return ua.Max_temp - } - if ua.SEK_price >= ua.Max_price { - return ua.Min_temp - } - - k := -(ua.Max_temp - ua.Min_temp) / (ua.Max_price - ua.Min_price) - //m := max_temp - (k * min_price) - //m := max_temp - desired_temp := k*(ua.SEK_price-ua.Min_price) + ua.Min_temp // y - y_min = k*(x-x_min), solve for y ("desired temp") - return desired_temp -} diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 12d44a5..6de8564 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -48,8 +48,8 @@ func main() { if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, cleanup := newResource(uac, &sys, servsTemp) - defer cleanup() + ua, startup := newResource(uac, &sys, servsTemp) + startup() sys.UAssets[ua.GetName()] = &ua } @@ -59,6 +59,9 @@ func main() { // Register the (system) and its services usecases.RegisterServices(&sys) + // Find zigbee gateway and store it in a global variable for reuse + findGateway() + // start the http handler and server go usecases.SetoutServers(&sys) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 1f352fe..e65f087 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -126,8 +126,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi }, } - findGateway(ua) - var ref components.Service for _, s := range servs { if s.Definition == "setpoint" { @@ -137,15 +135,13 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) - if uac.Model == "SmartThermostat" { - ua.sendSetPoint() - } else if uac.Model == "SmartPlug" { - // start the unit asset(s) - go ua.feedbackLoop(sys.Ctx) - } - return ua, func() { - log.Println("Shutting down zigbeevalve ", ua.Name) + if ua.Model == "SmartThermostat" { + ua.sendSetPoint() + } else if ua.Model == "SmartPlug" { + // start the unit asset(s) + go ua.feedbackLoop(ua.Owner.Ctx) + } } } @@ -190,7 +186,9 @@ func (ua *UnitAsset) processFeedbackLoop() { } -func findGateway(ua *UnitAsset) { +var gateway string + +func findGateway() { // https://pkg.go.dev/net/http#Get // GET https://phoscon.de/discover // to find gateways, array of JSONs is returned in http body, we'll only have one so take index 0 // GET the gateway through phoscons built in discover tool, the get will return a response, and in its body an array with JSON elements @@ -222,7 +220,7 @@ func findGateway(ua *UnitAsset) { } // Save the gateway to our unitasset s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) - ua.gateway = s + gateway = s //log.Println("Gateway found:", s) } @@ -247,7 +245,7 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) sendSetPoint() { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + ua.gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload @@ -257,7 +255,7 @@ func (ua *UnitAsset) sendSetPoint() { func (ua *UnitAsset) toggleState(state bool) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + ua.gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 009246d..eecefaf 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -138,10 +138,36 @@ func TestNewResource(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var uac UnitAsset + uac := UnitAsset{ + Name: "Test", + Model: "SmartThermostat", + } + sys := components.NewSystem("testsys", ctx) - servsTemp := []components.Service{} - ua, cleanup := newResource(uac, &sys, servsTemp) + sys.Husk = &components.Husk{ + Description: " is a controller for smart thermostats connected with a RaspBee II", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", + } + + setPointService := components.Service{ + Definition: "setpoint", + SubPath: "setpoint", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current thermal setpoint (GET) or sets it (PUT)", + } + // TODO: fix servsTemp and make sure the test works + servsTemp := components.Service{ + setPointService.SubPath: &setPointService, + } + + ua, _ := newResource(uac, &sys, servsTemp) + + if ua.GetName() != uac.Name { + t.Errorf("Expected ua.Name to be %s, but it was %s", uac.Name, ua.GetName()) + } } From 9ea6be5007e7ba0524f7f39771a7fed4fc8f5772 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 02:53:43 +0100 Subject: [PATCH 05/25] Fixed test for newResource, now working --- ZigBeeValve/zigbee_test.go | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index eecefaf..c594335 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/json" "fmt" "io" + "log" "net/http" "strings" "testing" @@ -11,6 +13,7 @@ import ( "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" + "github.com/sdoque/mbaigo/usecases" ) // mockTransport is used for replacing the default network Transport (used by @@ -138,11 +141,6 @@ func TestNewResource(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - uac := UnitAsset{ - Name: "Test", - Model: "SmartThermostat", - } - sys := components.NewSystem("testsys", ctx) sys.Husk = &components.Husk{ @@ -153,21 +151,22 @@ func TestNewResource(t *testing.T) { InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", } - setPointService := components.Service{ - Definition: "setpoint", - SubPath: "setpoint", - Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, - Description: "provides the current thermal setpoint (GET) or sets it (PUT)", - } + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate - // TODO: fix servsTemp and make sure the test works - servsTemp := components.Service{ - setPointService.SubPath: &setPointService, + rawResources, servsTemp, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) } - - ua, _ := newResource(uac, &sys, servsTemp) - - if ua.GetName() != uac.Name { - t.Errorf("Expected ua.Name to be %s, but it was %s", uac.Name, ua.GetName()) + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac UnitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, startup := newResource(uac, &sys, servsTemp) + startup() + sys.UAssets[ua.GetName()] = &ua } } From d6de1892c4bf7b4dd31e9ef851a0534f7ba248d3 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 03:06:23 +0100 Subject: [PATCH 06/25] added systemconfig template because it's needed for the tests --- ZigBeeValve/systemconfig.json | 74 +++++++++++++++++++++++++++++++++++ ZigBeeValve/thing.go | 2 +- ZigBeeValve/zigbee_test.go | 1 + 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 ZigBeeValve/systemconfig.json diff --git a/ZigBeeValve/systemconfig.json b/ZigBeeValve/systemconfig.json new file mode 100644 index 0000000..ae507a5 --- /dev/null +++ b/ZigBeeValve/systemconfig.json @@ -0,0 +1,74 @@ +{ + "systemname": "testsys", + "unit_assets": [ + { + "name": "Template", + "details": { + "Location": [ + "Kitchen" + ] + }, + "model": "", + "period": 10, + "setpoint": 20, + "APIkey": "" + } + ], + "services": [ + { + "servicedefinition": "setpoint", + "details": { + "Forms": [ + "SignalA_v1a" + ], + "Unit": [ + "Celsius" + ] + }, + "registrationPeriod": 0, + "costUnit": "" + } + ], + "protocolsNports": { + "coap": 0, + "http": 8870, + "https": 0 + }, + "distinguishedName": { + "Country": [ + "SE" + ], + "Organization": [ + "Luleaa University of Technology" + ], + "OrganizationalUnit": [ + "CPS" + ], + "Locality": [ + "Luleaa" + ], + "Province": [ + "Norrbotten" + ], + "StreetAddress": null, + "PostalCode": null, + "SerialNumber": "", + "CommonName": "arrowhead.eu", + "Names": null, + "ExtraNames": null + }, + "coreSystems": [ + { + "coresystem": "serviceregistrar", + "url": "http://localhost:8443/serviceregistrar/registry" + }, + { + "coresystem": "orchestrator", + "url": "http://localhost:8445/orchestrator/orchestration" + }, + { + "coresystem": "ca", + "url": "http://localhost:9000/ca/certification" + } + ] +} \ No newline at end of file diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index e65f087..ae7f497 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -254,7 +254,7 @@ func (ua *UnitAsset) sendSetPoint() { } func (ua *UnitAsset) toggleState(state bool) { - // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config + // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" // Create http friendly payload diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index c594335..1f397f5 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -159,6 +159,7 @@ func TestNewResource(t *testing.T) { if err != nil { log.Fatalf("Configuration error: %v\n", err) } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { var uac UnitAsset From 99963489a0b0affd0d4e7ae9266d34aa84f79d84 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 04:01:59 +0100 Subject: [PATCH 07/25] test --- Makefile | 11 +++++------ ZigBeeValve/zigbee_test.go | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 6b6d159..b582289 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +# Updates 3rd party packages and tools +deps: + go mod download + go install github.com/securego/gosec/v2/cmd/gosec@latest + go install github.com/fzipp/gocyclo/cmd/gocyclo@latest # Run tests and log the test coverage test: @@ -21,12 +26,6 @@ analyse: @echo -e "\nCYCLOMATIC COMPLEXITY\n====================" gocyclo -avg -top 10 . -# Updates 3rd party packages and tools -deps: - go mod download - go install github.com/securego/gosec/v2/cmd/gosec@latest - go install github.com/fzipp/gocyclo/cmd/gocyclo@latest - # Show documentation of public parts of package, in the current dir docs: go doc -all diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 1f397f5..afe77a2 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -171,3 +171,43 @@ func TestNewResource(t *testing.T) { sys.UAssets[ua.GetName()] = &ua } } + +func TestfeedbackLoop() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // instantiate the System + sys := components.NewSystem("testsys", ctx) + + // Instatiate the Capusle + sys.Husk = &components.Husk{ + Description: " is a controller for smart thermostats connected with a RaspBee II", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", + } + + // instantiate a template unit asset + assetTemplate := initTemplate() + assetName := assetTemplate.GetName() + sys.UAssets[assetName] = &assetTemplate + + // Configure the system + rawResources, servsTemp, err := usecases.Configure(&sys) + if err != nil { + log.Fatalf("Configuration error: %v\n", err) + } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) + for _, raw := range rawResources { + var uac UnitAsset + if err := json.Unmarshal(raw, &uac); err != nil { + log.Fatalf("Resource configuration error: %+v\n", err) + } + ua, startup := newResource(uac, &sys, servsTemp) + startup() + sys.UAssets[ua.GetName()] = &ua + } + + // TODO: Test feedbackloop and processfeedbackloop +} From 50d30ec49afef0468f082a5fb34c124379af1f1a Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 04:07:07 +0100 Subject: [PATCH 08/25] test2 --- Makefile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index b582289..3a509b6 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,3 @@ -# Updates 3rd party packages and tools -deps: - go mod download - go install github.com/securego/gosec/v2/cmd/gosec@latest - go install github.com/fzipp/gocyclo/cmd/gocyclo@latest # Run tests and log the test coverage test: @@ -14,6 +9,8 @@ bench: # Runs source code linters and catches common errors lint: + enable: + - gocyclo test -z $$(gofmt -l .) || (echo "Code isn't gofmt'ed!" && exit 1) go vet $$(go list ./... | grep -v /tmp) gosec -quiet -fmt=golint -exclude-dir="tmp" ./... @@ -26,6 +23,12 @@ analyse: @echo -e "\nCYCLOMATIC COMPLEXITY\n====================" gocyclo -avg -top 10 . +# Updates 3rd party packages and tools +deps: + go mod download + go install github.com/securego/gosec/v2/cmd/gosec@latest + go install github.com/fzipp/gocyclo/cmd/gocyclo@latest + # Show documentation of public parts of package, in the current dir docs: go doc -all From 4a1a5d7ebdfc14609f0da7ef3d443530c3af3f71 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 04:08:08 +0100 Subject: [PATCH 09/25] Makefile back to normal --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 3a509b6..6b6d159 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,6 @@ bench: # Runs source code linters and catches common errors lint: - enable: - - gocyclo test -z $$(gofmt -l .) || (echo "Code isn't gofmt'ed!" && exit 1) go vet $$(go list ./... | grep -v /tmp) gosec -quiet -fmt=golint -exclude-dir="tmp" ./... From ba91d61d6725f4e0a7b09f85cb635772efcdb287 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 27 Jan 2025 12:06:01 +0100 Subject: [PATCH 10/25] Fixed workflow (git tests) --- .github/workflows/main.yml | 2 ++ ZigBeeValve/thing.go | 2 -- ZigBeeValve/zigbee_test.go | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1d0f615..d95f148 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.23 + - name: Install dependencies + run: make deps - name: Run tests run: make test - name: Report stats diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index ae7f497..a91bf49 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -182,8 +182,6 @@ func (ua *UnitAsset) processFeedbackLoop() { } else { ua.toggleState(false) } - //log.Println("Feedback loop done.") - } var gateway string diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index afe77a2..db5c28f 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -166,20 +166,18 @@ func TestNewResource(t *testing.T) { if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, startup := newResource(uac, &sys, servsTemp) - startup() + ua, _ := newResource(uac, &sys, servsTemp) + //startup() sys.UAssets[ua.GetName()] = &ua } } -func TestfeedbackLoop() { +func TestFeedbackLoop(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - // instantiate the System sys := components.NewSystem("testsys", ctx) - // Instatiate the Capusle sys.Husk = &components.Husk{ Description: " is a controller for smart thermostats connected with a RaspBee II", Certificate: "ABCD", @@ -188,26 +186,30 @@ func TestfeedbackLoop() { InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", } - // instantiate a template unit asset assetTemplate := initTemplate() assetName := assetTemplate.GetName() sys.UAssets[assetName] = &assetTemplate - // Configure the system rawResources, servsTemp, err := usecases.Configure(&sys) if err != nil { log.Fatalf("Configuration error: %v\n", err) } + sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) for _, raw := range rawResources { var uac UnitAsset if err := json.Unmarshal(raw, &uac); err != nil { log.Fatalf("Resource configuration error: %+v\n", err) } - ua, startup := newResource(uac, &sys, servsTemp) - startup() + ua, _ := newResource(uac, &sys, servsTemp) + //startup() sys.UAssets[ua.GetName()] = &ua } // TODO: Test feedbackloop and processfeedbackloop + /* + ua.feedbackLoop(ctx) + cancel() + time.Sleep(2 * time.Second) + */ } From 665be03818740573c64893d6329516a3331e84fe Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 28 Jan 2025 14:05:25 +0100 Subject: [PATCH 11/25] Added new tests, work in progress --- ZigBeeValve/thing.go | 22 +++---- ZigBeeValve/zigbee_test.go | 121 +++++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 66 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index a91bf49..26c155c 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -18,8 +18,7 @@ import ( "github.com/sdoque/mbaigo/usecases" ) -//------------------------------------ Used when discovering the gateway - +// ------------------------------------ Used when discovering the gateway type discoverJSON struct { Id string `json:"id"` Internalipaddress string `json:"internalipaddress"` @@ -39,11 +38,10 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Model string `json:"model"` - Period time.Duration `json:"period"` - Setpt float64 `json:"setpoint"` - gateway string - Apikey string `json:"APIkey"` + Model string `json:"model"` + Period time.Duration `json:"period"` + Setpt float64 `json:"setpoint"` + Apikey string `json:"APIkey"` } // GetName returns the name of the Resource. @@ -87,8 +85,7 @@ func initTemplate() components.UnitAsset { Model: "", Period: 10, Setpt: 20, - gateway: "", - Apikey: "", + Apikey: "1234", ServicesMap: components.Services{ setPointService.SubPath: &setPointService, }, @@ -119,7 +116,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi Model: uac.Model, Period: uac.Period, Setpt: uac.Setpt, - gateway: uac.gateway, Apikey: uac.Apikey, CervicesMap: components.Cervices{ t.Name: t, @@ -216,10 +212,10 @@ func findGateway() { log.Println("No gateway was found") return } - // Save the gateway to our unitasset + // Save the gateway s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) gateway = s - //log.Println("Gateway found:", s) + log.Println("Gateway found:", s) } //-------------------------------------Thing's resource methods @@ -281,7 +277,7 @@ func sendRequest(data []byte, apiURL string) { defer resp.Body.Close() b, err := io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) + log.Println("Something went wrong while reading payload into body variable, error:", err) return } if resp.StatusCode > 299 { diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index db5c28f..8181516 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -9,7 +9,6 @@ import ( "net/http" "strings" "testing" - "time" "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" @@ -45,21 +44,21 @@ func (t mockTransport) domainHits(domain string) int { // TODO: this might need to be expanded to a full JSON array? -const priceExample string = `[{ - "SEK_per_kWh": 0.26673, - "EUR_per_kWh": 0.02328, - "EXR": 11.457574, - "time_start": "2025-01-06T%02d:00:00+01:00", - "time_end": "2025-01-06T%02d:00:00+01:00" -}]` +const discoverExample string = `[{ + "Id": "123", + "Internalipaddress": "localhost", + "Macaddress": "test", + "Internalport": 8080, + "Name": "My gateway", + "Publicipaddress": "test" + }]` // RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). // It prevents the request from being sent over the network and count how many times // a domain was requested. func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - hour := time.Now().Local().Hour() - fakeBody := fmt.Sprintf(priceExample, hour, hour+1) + fakeBody := fmt.Sprint(discoverExample) // TODO: should be able to adjust these return values for the error cases resp = &http.Response{ Status: "200 OK", @@ -73,14 +72,7 @@ func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err er //////////////////////////////////////////////////////////////////////////////// -const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" -const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" - func TestUnitAsset(t *testing.T) { - - // Don't understand how to check my own deConz API calls, will extend the test with this once i understand - trans := newMockTransport() - // Create a form f := forms.SignalA_v1a{ Value: 27.0, @@ -96,10 +88,11 @@ func TestUnitAsset(t *testing.T) { t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) } - // TODO: Add api call to make sure it only sends update to HW once. - hits := trans.domainHits(thermostatDomain) - if hits > 1 { - t.Errorf("Expected number of api requests = 1, got %d requests", hits) + // Fetch Setpt w/ a form + f2 := ua.getSetPoint() + + if f2.Value != f.Value { + t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) } } @@ -172,44 +165,66 @@ func TestNewResource(t *testing.T) { } } -func TestFeedbackLoop(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sys := components.NewSystem("testsys", ctx) +const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" +const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" - sys.Husk = &components.Husk{ - Description: " is a controller for smart thermostats connected with a RaspBee II", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, - InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", - } +func TestProcessfeedbackLoop(t *testing.T) { + // Don't know how to test this +} - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate +func TestFindGateway1(t *testing.T) { + // New mocktransport + gatewayDomain := "https://phoscon.de/discover" + trans := newMockTransport() - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) + // ---- All ok! ---- + findGateway() + if gateway != "localhost:8080" { + t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } - - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, _ := newResource(uac, &sys, servsTemp) - //startup() - sys.UAssets[ua.GetName()] = &ua + hits := trans.domainHits(gatewayDomain) + if hits > 1 { + t.Fatalf("Too many hits on gatewayDomain, expected 1 got, %d", hits) } - // TODO: Test feedbackloop and processfeedbackloop + // Have to make changes to mockTransport to test this? + // ---- Error cases ---- /* - ua.feedbackLoop(ctx) - cancel() - time.Sleep(2 * time.Second) + // Couldn't find gateway + findGateway() + if gateway != "" { + log.Printf("Expected not to find gateway, found %s", gateway) + } */ + // Statuscode > 299, have to make changes to mockTransport to test this + // Couldn't read body, have to make changes to mockTransport to test this + // Error during unmarshal, have to make changes to mockTransport to test this +} + +const zigbeeGateway string = "http://localhost:8080/" + +func TestToggleState(t *testing.T) { + trans := newMockTransport() + + ua := initTemplate().(*UnitAsset) + + ua.toggleState(true) + + hits := trans.domainHits(plugDomain) + if hits > 1 { + t.Errorf("Expected one hit, got %d", hits) + } +} + +func TestSendSetPoint(t *testing.T) { + trans := newMockTransport() + + ua := initTemplate().(*UnitAsset) + + ua.sendSetPoint() + + hits := trans.domainHits(thermostatDomain) + if hits > 1 { + t.Errorf("expected one hit, got %d", hits) + } } From 39a4dc458188b1c4469e8a547049c5a3cb105d12 Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 28 Jan 2025 17:41:38 +0100 Subject: [PATCH 12/25] Added new tests etc --- ZigBeeValve/ZigBeeValve.go | 11 ++- ZigBeeValve/thing.go | 73 ++++++++++++-------- ZigBeeValve/zigbee_test.go | 135 +++++++++++++++++++++++++------------ 3 files changed, 148 insertions(+), 71 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 6de8564..9fce96a 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -60,7 +60,10 @@ func main() { usecases.RegisterServices(&sys) // Find zigbee gateway and store it in a global variable for reuse - findGateway() + err = findGateway() + if err != nil { + log.Fatal("Error getting gateway, shutting down:", err) + } // start the http handler and server go usecases.SetoutServers(&sys) @@ -97,7 +100,11 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { log.Println("Model:", rsc.Model) rsc.setSetPoint(sig) if rsc.Model == "SmartThermostat" { - rsc.sendSetPoint() + err = rsc.sendSetPoint() + if err != nil { + log.Println("Error sending setpoint:", err) + http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) + } } default: diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 26c155c..cfd3038 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -133,7 +133,11 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi return ua, func() { if ua.Model == "SmartThermostat" { - ua.sendSetPoint() + err := ua.sendSetPoint() + if err != nil { + log.Println("Error occured:", err) + // TODO: Turn off system if this startup() fails + } } else if ua.Model == "SmartPlug" { // start the unit asset(s) go ua.feedbackLoop(ua.Owner.Ctx) @@ -174,48 +178,61 @@ func (ua *UnitAsset) processFeedbackLoop() { // TODO: Check diff instead of a hard over/under value? meaning it'll only turn on/off if diff is over 0.5 degrees if tup.Value < ua.Setpt { - ua.toggleState(true) + err = ua.toggleState(true) + if err != nil { + log.Println("Error occured: ", err) + } } else { - ua.toggleState(false) + err = ua.toggleState(false) + if err != nil { + log.Println("Error occured: ", err) + } } } var gateway string -func findGateway() { +const discoveryURL string = "https://phoscon.de/discover" + +var errStatusCode error = fmt.Errorf("bad status code") +var errMissingGateway error = fmt.Errorf("missing gateway") + +func findGateway() (err error) { // https://pkg.go.dev/net/http#Get // GET https://phoscon.de/discover // to find gateways, array of JSONs is returned in http body, we'll only have one so take index 0 // GET the gateway through phoscons built in discover tool, the get will return a response, and in its body an array with JSON elements // ours is index 0 since there's no other RaspBee/ZigBee gateways on the network - res, err := http.Get("https://phoscon.de/discover") + res, err := http.Get(discoveryURL) if err != nil { - log.Println("Couldn't get gateway, error:", err) + //log.Println("Couldn't get gateway, error:", err) return } defer res.Body.Close() if res.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\n", res.StatusCode) - return + //log.Printf("Response failed with status code: %d and\n", res.StatusCode) + return errStatusCode } body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { - log.Println("Something went wrong while reading the body during discovery, error:", err) + //log.Println("Something went wrong while reading the body during discovery, error:", err) return } var gw []discoverJSON // Create a list to hold the gateway json err = json.Unmarshal(body, &gw) // "unpack" body from []byte to []discoverJSON, save errors if err != nil { - log.Println("Error during Unmarshal, error:", err) + //log.Println("Error during Unmarshal, error:", err) return } + if len(gw) < 1 { - log.Println("No gateway was found") - return + //log.Println("No gateway was found") + return errMissingGateway } // Save the gateway s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) gateway = s - log.Println("Gateway found:", s) + //log.Println("Gateway found:", s) + return } //-------------------------------------Thing's resource methods @@ -237,33 +254,34 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { log.Println("*---------------------*") } -func (ua *UnitAsset) sendSetPoint() { +func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload data := []byte(s) // Turned into byte array - sendRequest(data, apiURL) + return sendRequest(data, apiURL) } -func (ua *UnitAsset) toggleState(state bool) { +func (ua *UnitAsset) toggleState(state bool) (err error) { // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload data := []byte(s) // Turned into byte array - sendRequest(data, apiURL) + err = sendRequest(data, apiURL) + return err } -func sendRequest(data []byte, apiURL string) { +func sendRequest(data []byte, apiURL string) (err error) { body := bytes.NewBuffer(data) // Put data into buffer req, err := http.NewRequest(http.MethodPut, apiURL, body) // Put request is made if err != nil { - log.Println("Error making new HTTP PUT request, error:", err) - return + // log.Println("Error making new HTTP PUT request, error:", err) + return err } req.Header.Set("Content-Type", "application/json") // Make sure it's JSON @@ -271,17 +289,18 @@ func sendRequest(data []byte, apiURL string) { client := &http.Client{} // Make a client resp, err := client.Do(req) // Perform the put request if err != nil { - log.Println("Error sending HTTP PUT request, error:", err) - return + // log.Println("Error sending HTTP PUT request, error:", err) + return err } defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) // Read the payload into body variable + _, err = io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { - log.Println("Something went wrong while reading payload into body variable, error:", err) - return + // log.Println("Something went wrong while reading payload into body variable, error:", err) + return err } if resp.StatusCode > 299 { - log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) - return + // log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) + return err } + return } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 8181516..262a721 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -19,12 +19,16 @@ import ( // http.DefaultClient) and it will intercept network requests. type mockTransport struct { + resp *http.Response hits map[string]int + err error } -func newMockTransport() mockTransport { +func newMockTransport(resp *http.Response, err error) mockTransport { t := mockTransport{ + resp: resp, hits: make(map[string]int), + err: err, } // Highjack the default http client so no actuall http requests are sent over the network http.DefaultClient.Transport = t @@ -58,16 +62,12 @@ const discoverExample string = `[{ // a domain was requested. func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - fakeBody := fmt.Sprint(discoverExample) - // TODO: should be able to adjust these return values for the error cases - resp = &http.Response{ - Status: "200 OK", - StatusCode: 200, - Request: req, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } + t.resp.Request = req t.hits[req.URL.Hostname()] += 1 - return + if t.err != nil { + return nil, t.err + } + return t.resp, nil } //////////////////////////////////////////////////////////////////////////////// @@ -168,63 +168,114 @@ func TestNewResource(t *testing.T) { const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" -func TestProcessfeedbackLoop(t *testing.T) { - // Don't know how to test this +type errReader int + +var errBodyRead error = fmt.Errorf("bad body read") + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} + +func (errReader) Close() error { + return nil } -func TestFindGateway1(t *testing.T) { +func TestFindGateway(t *testing.T) { // New mocktransport - gatewayDomain := "https://phoscon.de/discover" - trans := newMockTransport() + //gatewayDomain := "https://phoscon.de/discover" + + fakeBody := fmt.Sprint(discoverExample) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + newMockTransport(resp, nil) // ---- All ok! ---- - findGateway() + err := findGateway() + + if err != nil { + t.Fatal("Gatewayn not found", err) + } if gateway != "localhost:8080" { t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } - hits := trans.domainHits(gatewayDomain) - if hits > 1 { - t.Fatalf("Too many hits on gatewayDomain, expected 1 got, %d", hits) - } // Have to make changes to mockTransport to test this? // ---- Error cases ---- - /* - // Couldn't find gateway - findGateway() - if gateway != "" { - log.Printf("Expected not to find gateway, found %s", gateway) - } - */ + + // Unmarshall error + newMockTransport(resp, fmt.Errorf("Test error")) + err = findGateway() + if err == nil { + t.Error("Error expcted, got nil instead", err) + } + // Statuscode > 299, have to make changes to mockTransport to test this - // Couldn't read body, have to make changes to mockTransport to test this - // Error during unmarshal, have to make changes to mockTransport to test this -} + resp.StatusCode = 300 + newMockTransport(resp, nil) + err = findGateway() + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } + + // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body + resp.StatusCode = 200 + resp.Body = errReader(0) + newMockTransport(resp, nil) + err = findGateway() + if err != errBodyRead { + t.Error("Expected error") + } + + // Actual http body is unmarshaled correctly + resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) + newMockTransport(resp, nil) + err = findGateway() + if err == nil { + t.Error("Expected error") + } -const zigbeeGateway string = "http://localhost:8080/" + // Empty list of gateways + resp.Body = io.NopCloser(strings.NewReader("[]")) + newMockTransport(resp, nil) + err = findGateway() + if err != errMissingGateway { + t.Error("Expected error", err) + } +} func TestToggleState(t *testing.T) { - trans := newMockTransport() + fakeBody := fmt.Sprint("") + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, nil) ua := initTemplate().(*UnitAsset) ua.toggleState(true) - - hits := trans.domainHits(plugDomain) - if hits > 1 { - t.Errorf("Expected one hit, got %d", hits) - } } func TestSendSetPoint(t *testing.T) { - trans := newMockTransport() + fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, nil) ua := initTemplate().(*UnitAsset) ua.sendSetPoint() - hits := trans.domainHits(thermostatDomain) - if hits > 1 { - t.Errorf("expected one hit, got %d", hits) - } } From 99dc342b9752b1afc1d95677e7c9c8dceb1bbe44 Mon Sep 17 00:00:00 2001 From: walpat-1 Date: Wed, 29 Jan 2025 16:53:29 +0100 Subject: [PATCH 13/25] Tested pretty much all the code possible in things.go, moving on to ZigBeeValve.go --- ZigBeeValve/thing.go | 42 +++++----- ZigBeeValve/zigbee_test.go | 158 ++++++++++++++++++++++++++++--------- 2 files changed, 141 insertions(+), 59 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index cfd3038..7669316 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -204,7 +204,6 @@ func findGateway() (err error) { // ours is index 0 since there's no other RaspBee/ZigBee gateways on the network res, err := http.Get(discoveryURL) if err != nil { - //log.Println("Couldn't get gateway, error:", err) return } defer res.Body.Close() @@ -214,13 +213,11 @@ func findGateway() (err error) { } body, err := io.ReadAll(res.Body) // Read the payload into body variable if err != nil { - //log.Println("Something went wrong while reading the body during discovery, error:", err) return } var gw []discoverJSON // Create a list to hold the gateway json err = json.Unmarshal(body, &gw) // "unpack" body from []byte to []discoverJSON, save errors if err != nil { - //log.Println("Error during Unmarshal, error:", err) return } @@ -260,8 +257,11 @@ func (ua *UnitAsset) sendSetPoint() (err error) { // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload - data := []byte(s) // Turned into byte array - return sendRequest(data, apiURL) + req, err := createRequest(s, apiURL) + if err != nil { + return + } + return sendRequest(req) } func (ua *UnitAsset) toggleState(state bool) (err error) { @@ -270,37 +270,37 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload - data := []byte(s) // Turned into byte array - err = sendRequest(data, apiURL) - return err + req, err := createRequest(s, apiURL) + if err != nil { + return + } + return sendRequest(req) } -func sendRequest(data []byte, apiURL string) (err error) { - body := bytes.NewBuffer(data) // Put data into buffer - - req, err := http.NewRequest(http.MethodPut, apiURL, body) // Put request is made +func createRequest(data string, apiURL string) (req *http.Request, err error) { + body := bytes.NewReader([]byte(data)) // Put data into buffer + req, err = http.NewRequest(http.MethodPut, apiURL, body) // Put request is made if err != nil { - // log.Println("Error making new HTTP PUT request, error:", err) - return err + return nil, err } req.Header.Set("Content-Type", "application/json") // Make sure it's JSON + return req, err +} - client := &http.Client{} // Make a client - resp, err := client.Do(req) // Perform the put request +func sendRequest(req *http.Request) (err error) { + resp, err := http.DefaultClient.Do(req) if err != nil { - // log.Println("Error sending HTTP PUT request, error:", err) return err } + defer resp.Body.Close() _, err = io.ReadAll(resp.Body) // Read the payload into body variable if err != nil { - // log.Println("Something went wrong while reading payload into body variable, error:", err) - return err + return } if resp.StatusCode > 299 { - // log.Printf("Response failed with status code: %d and\nbody: %s\n", resp.StatusCode, string(b)) - return err + return errStatusCode } return } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 262a721..e9d63cb 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -19,33 +19,24 @@ import ( // http.DefaultClient) and it will intercept network requests. type mockTransport struct { - resp *http.Response - hits map[string]int - err error + returnError bool + resp *http.Response + hits map[string]int + err error } -func newMockTransport(resp *http.Response, err error) mockTransport { +func newMockTransport(resp *http.Response, retErr bool, err error) mockTransport { t := mockTransport{ - resp: resp, - hits: make(map[string]int), - err: err, + returnError: retErr, + resp: resp, + hits: make(map[string]int), + err: err, } // Highjack the default http client so no actuall http requests are sent over the network http.DefaultClient.Transport = t return t } -// domainHits returns the number of requests to a domain (or -1 if domain wasn't found). - -func (t mockTransport) domainHits(domain string) int { - for u, hits := range t.hits { - if u == domain { - return hits - } - } - return -1 -} - // TODO: this might need to be expanded to a full JSON array? const discoverExample string = `[{ @@ -61,12 +52,19 @@ const discoverExample string = `[{ // It prevents the request from being sent over the network and count how many times // a domain was requested. +var errHTTP error = fmt.Errorf("bad http request") + func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - t.resp.Request = req t.hits[req.URL.Hostname()] += 1 if t.err != nil { return nil, t.err } + if t.returnError != false { + req.GetBody = func() (io.ReadCloser, error) { + return nil, errHTTP + } + } + t.resp.Request = req return t.resp, nil } @@ -165,9 +163,6 @@ func TestNewResource(t *testing.T) { } } -const thermostatDomain string = "http://localhost:8870/api/B3AFB6415A/sensors/2/config" -const plugDomain string = "http://localhost:8870/api/B3AFB6415A/lights/1/config" - type errReader int var errBodyRead error = fmt.Errorf("bad body read") @@ -180,10 +175,25 @@ func (errReader) Close() error { return nil } -func TestFindGateway(t *testing.T) { - // New mocktransport - //gatewayDomain := "https://phoscon.de/discover" +func TestProcessFeedbackLoop(t *testing.T) { + // TODO: Test as much of the code as possible. + // Maybe try to pass arguments to processFeedbackLoop, to skip the GetState() function? + /* + fakeBody := // Find out what a form looks like, and pass it to this test function + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + newMockTransport(resp, false, nil) + + ua := initTemplate().(*UnitAsset) + ua.processFeedbackLoop() + */ +} +func TestFindGateway(t *testing.T) { fakeBody := fmt.Sprint(discoverExample) resp := &http.Response{ @@ -191,7 +201,7 @@ func TestFindGateway(t *testing.T) { StatusCode: 200, Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) // ---- All ok! ---- err := findGateway() @@ -203,11 +213,10 @@ func TestFindGateway(t *testing.T) { t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } - // Have to make changes to mockTransport to test this? // ---- Error cases ---- // Unmarshall error - newMockTransport(resp, fmt.Errorf("Test error")) + newMockTransport(resp, false, fmt.Errorf("Test error")) err = findGateway() if err == nil { t.Error("Error expcted, got nil instead", err) @@ -215,7 +224,7 @@ func TestFindGateway(t *testing.T) { // Statuscode > 299, have to make changes to mockTransport to test this resp.StatusCode = 300 - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err != errStatusCode { t.Error("Expected errStatusCode, got", err) @@ -224,7 +233,7 @@ func TestFindGateway(t *testing.T) { // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body resp.StatusCode = 200 resp.Body = errReader(0) - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err != errBodyRead { t.Error("Expected error") @@ -232,7 +241,7 @@ func TestFindGateway(t *testing.T) { // Actual http body is unmarshaled correctly resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err == nil { t.Error("Expected error") @@ -240,7 +249,7 @@ func TestFindGateway(t *testing.T) { // Empty list of gateways resp.Body = io.NopCloser(strings.NewReader("[]")) - newMockTransport(resp, nil) + newMockTransport(resp, false, nil) err = findGateway() if err != errMissingGateway { t.Error("Expected error", err) @@ -248,7 +257,7 @@ func TestFindGateway(t *testing.T) { } func TestToggleState(t *testing.T) { - fakeBody := fmt.Sprint("") + fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) resp := &http.Response{ Status: "200 OK", @@ -256,11 +265,15 @@ func TestToggleState(t *testing.T) { Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, nil) - + newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) - + // All ok! ua.toggleState(true) + // Error + // change gateway to bad character/url, return gateway to original value + gateway = brokenURL + ua.toggleState(true) + findGateway() } func TestSendSetPoint(t *testing.T) { @@ -272,10 +285,79 @@ func TestSendSetPoint(t *testing.T) { Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, nil) - + newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) + // All ok! + gateway = "" + err := ua.sendSetPoint() + if err != nil { + t.Error("Unexpected error:", err) + } + // Error + gateway = brokenURL ua.sendSetPoint() + findGateway() + +} + +// func createRequest(data string, apiURL string) (req *http.Request, err error) +func TestCreateRequest(t *testing.T) { + data := "test" + apiURL := "http://localhost:8080/test" + + _, err := createRequest(data, apiURL) + if err != nil { + t.Error("Error occured, expected none") + } + + _, err = createRequest(data, brokenURL) + if err == nil { + t.Error("Expected error") + } + +} + +var brokenURL string = string([]byte{0x7f}) + +func TestSendRequest(t *testing.T) { + // Set up standard response & catch http requests + fakeBody := fmt.Sprint(`Test`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + // All ok! + newMockTransport(resp, false, nil) + apiURL := "http://localhost:8080/test" + s := fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload + req, _ := createRequest(s, apiURL) + err := sendRequest(req) + if err != nil { + t.Error("Expected no errors, error occured:", err) + } + + // Error unpacking body + resp.Body = errReader(0) + newMockTransport(resp, false, nil) + + err = sendRequest(req) + + if err == nil { + t.Error("Expected errors, no error occured:") + } + + // Error StatusCode + resp.Body = io.NopCloser(strings.NewReader(fakeBody)) + resp.StatusCode = 300 + newMockTransport(resp, false, nil) + err = sendRequest(req) + + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } } From e54fbd535251bf45d34837a29e6a2bbdf95acb78 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 3 Feb 2025 11:40:41 +0100 Subject: [PATCH 14/25] changed tests, and added new tests --- ZigBeeValve/ZigBeeValve.go | 13 +- ZigBeeValve/systemconfig.json | 74 ------ ZigBeeValve/thing.go | 10 +- ZigBeeValve/thing_test.go | 353 +++++++++++++++++++++++++++++ ZigBeeValve/zigbee_test.go | 408 ++++++---------------------------- 5 files changed, 429 insertions(+), 429 deletions(-) delete mode 100644 ZigBeeValve/systemconfig.json create mode 100644 ZigBeeValve/thing_test.go diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 9fce96a..148741f 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -62,7 +62,7 @@ func main() { // Find zigbee gateway and store it in a global variable for reuse err = findGateway() if err != nil { - log.Fatal("Error getting gateway, shutting down:", err) + log.Fatal("Error getting gateway, shutting down: ", err) } // start the http handler and server @@ -93,17 +93,18 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error with the setting desired temp ", err) + log.Println("Error processing request") + http.Error(w, "Request incorrectly formated", http.StatusBadRequest) + return } - log.Println("sig:", sig) - log.Println("URL:", r.URL) - log.Println("Model:", rsc.Model) + rsc.setSetPoint(sig) if rsc.Model == "SmartThermostat" { err = rsc.sendSetPoint() if err != nil { - log.Println("Error sending setpoint:", err) + log.Println("Error sending setpoint:") http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) + return } } diff --git a/ZigBeeValve/systemconfig.json b/ZigBeeValve/systemconfig.json deleted file mode 100644 index ae507a5..0000000 --- a/ZigBeeValve/systemconfig.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "systemname": "testsys", - "unit_assets": [ - { - "name": "Template", - "details": { - "Location": [ - "Kitchen" - ] - }, - "model": "", - "period": 10, - "setpoint": 20, - "APIkey": "" - } - ], - "services": [ - { - "servicedefinition": "setpoint", - "details": { - "Forms": [ - "SignalA_v1a" - ], - "Unit": [ - "Celsius" - ] - }, - "registrationPeriod": 0, - "costUnit": "" - } - ], - "protocolsNports": { - "coap": 0, - "http": 8870, - "https": 0 - }, - "distinguishedName": { - "Country": [ - "SE" - ], - "Organization": [ - "Luleaa University of Technology" - ], - "OrganizationalUnit": [ - "CPS" - ], - "Locality": [ - "Luleaa" - ], - "Province": [ - "Norrbotten" - ], - "StreetAddress": null, - "PostalCode": null, - "SerialNumber": "", - "CommonName": "arrowhead.eu", - "Names": null, - "ExtraNames": null - }, - "coreSystems": [ - { - "coresystem": "serviceregistrar", - "url": "http://localhost:8443/serviceregistrar/registry" - }, - { - "coresystem": "orchestrator", - "url": "http://localhost:8445/orchestrator/orchestration" - }, - { - "coresystem": "ca", - "url": "http://localhost:9000/ca/certification" - } - ] -} \ No newline at end of file diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 7669316..2fc9689 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -82,7 +82,7 @@ func initTemplate() components.UnitAsset { uat := &UnitAsset{ Name: "Template", Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "", + Model: "SmartThermostat", Period: 10, Setpt: 20, Apikey: "1234", @@ -246,9 +246,11 @@ func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { // setSetPoint updates the thermal setpoint func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { ua.Setpt = f.Value - log.Println("*---------------------*") - log.Printf("New set point: %.1f\n", f.Value) - log.Println("*---------------------*") + /* + log.Println("*---------------------*") + log.Printf("New set point: %.1f\n", f.Value) + log.Println("*---------------------*") + */ } func (ua *UnitAsset) sendSetPoint() (err error) { diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go new file mode 100644 index 0000000..0c6f684 --- /dev/null +++ b/ZigBeeValve/thing_test.go @@ -0,0 +1,353 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/sdoque/mbaigo/components" + "github.com/sdoque/mbaigo/forms" +) + +// mockTransport is used for replacing the default network Transport (used by +// http.DefaultClient) and it will intercept network requests. + +type mockTransport struct { + returnError bool + resp *http.Response + hits map[string]int + err error +} + +func newMockTransport(resp *http.Response, retErr bool, err error) mockTransport { + t := mockTransport{ + returnError: retErr, + resp: resp, + hits: make(map[string]int), + err: err, + } + // Highjack the default http client so no actuall http requests are sent over the network + http.DefaultClient.Transport = t + return t +} + +// TODO: this might need to be expanded to a full JSON array? + +const discoverExample string = `[{ + "Id": "123", + "Internalipaddress": "localhost", + "Macaddress": "test", + "Internalport": 8080, + "Name": "My gateway", + "Publicipaddress": "test" + }]` + +// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). +// It prevents the request from being sent over the network and count how many times +// a domain was requested. + +var errHTTP error = fmt.Errorf("bad http request") + +func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + t.hits[req.URL.Hostname()] += 1 + if t.err != nil { + return nil, t.err + } + if t.returnError != false { + req.GetBody = func() (io.ReadCloser, error) { + return nil, errHTTP + } + } + t.resp.Request = req + return t.resp, nil +} + +//////////////////////////////////////////////////////////////////////////////// + +func TestUnitAsset(t *testing.T) { + // Create a form + f := forms.SignalA_v1a{ + Value: 27.0, + } + + // Creates a single UnitAsset and assert it changes + ua := initTemplate().(*UnitAsset) + + // Change Setpt + ua.setSetPoint(f) + + if ua.Setpt != 27.0 { + t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) + } + + // Fetch Setpt w/ a form + f2 := ua.getSetPoint() + + if f2.Value != f.Value { + t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) + } +} + +func TestGetters(t *testing.T) { + ua := initTemplate().(*UnitAsset) + + name := ua.GetName() + if name != "Template" { + t.Errorf("Expected name to be 2, instead got %s", name) + } + + services := ua.GetServices() + if services == nil { + t.Fatalf("Expected services not to be nil") + } + if services["setpoint"].Definition != "setpoint" { + t.Errorf("Expected definition to be setpoint") + } + + details := ua.GetDetails() + if details == nil { + t.Fatalf("Details was nil, expected map") + } + if len(details["Location"]) == 0 { + t.Fatalf("Location was nil, expected kitchen") + } + + if details["Location"][0] != "Kitchen" { + t.Errorf("Expected location to be Kitchen") + } + + cervices := ua.GetCervices() + if cervices != nil { + t.Errorf("Expected no cervices") + } +} + +func TestNewResource(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + sys := components.NewSystem("testsys", ctx) + sys.Husk = &components.Husk{ + Description: " is a controller for smart thermostats connected with a RaspBee II", + Certificate: "ABCD", + Details: map[string][]string{"Developer": {"Arrowhead"}}, + ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, + InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", + } + + setPointService := components.Service{ + Definition: "setpoint", + SubPath: "setpoint", + Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current thermal setpoint (GET) or sets it (PUT)", + } + + uac := UnitAsset{ + Name: "Template", + Details: map[string][]string{"Location": {"Kitchen"}}, + Model: "SmartThermostat", + Period: 10, + Setpt: 20, + Apikey: "1234", + ServicesMap: components.Services{ + setPointService.SubPath: &setPointService, + }, + } + + ua, _ := newResource(uac, &sys, nil) + // Happy test case: + //Get a unitasset with correct values + name := ua.GetName() + if name != "Template" { + t.Errorf("Expected name to be Template, but instead got: %v", name) + } +} + +type errReader int + +var errBodyRead error = fmt.Errorf("bad body read") + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errBodyRead +} + +func (errReader) Close() error { + return nil +} + +func TestProcessFeedbackLoop(t *testing.T) { + // TODO: Test as much of the code as possible? +} + +func TestFindGateway(t *testing.T) { + fakeBody := fmt.Sprint(discoverExample) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + newMockTransport(resp, false, nil) + + // ---- All ok! ---- + err := findGateway() + + if err != nil { + t.Fatal("Gatewayn not found", err) + } + if gateway != "localhost:8080" { + t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) + } + + // ---- Error cases ---- + + // Unmarshall error + newMockTransport(resp, false, fmt.Errorf("Test error")) + err = findGateway() + if err == nil { + t.Error("Error expcted, got nil instead", err) + } + + // Statuscode > 299, have to make changes to mockTransport to test this + resp.StatusCode = 300 + newMockTransport(resp, false, nil) + err = findGateway() + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } + + // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body + resp.StatusCode = 200 + resp.Body = errReader(0) + newMockTransport(resp, false, nil) + err = findGateway() + if err != errBodyRead { + t.Error("Expected error") + } + + // Actual http body is unmarshaled correctly + resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) + newMockTransport(resp, false, nil) + err = findGateway() + if err == nil { + t.Error("Expected error") + } + + // Empty list of gateways + resp.Body = io.NopCloser(strings.NewReader("[]")) + newMockTransport(resp, false, nil) + err = findGateway() + if err != errMissingGateway { + t.Error("Expected error", err) + } +} + +func TestToggleState(t *testing.T) { + fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, false, nil) + ua := initTemplate().(*UnitAsset) + // All ok! + ua.toggleState(true) + // Error + // change gateway to bad character/url, return gateway to original value + gateway = brokenURL + ua.toggleState(true) + findGateway() +} + +func TestSendSetPoint(t *testing.T) { + fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + newMockTransport(resp, false, nil) + ua := initTemplate().(*UnitAsset) + // All ok! + gateway = "localhost" + err := ua.sendSetPoint() + if err != nil { + t.Error("Unexpected error:", err) + } + + // Error + gateway = brokenURL + ua.sendSetPoint() + findGateway() + gateway = "localhost" +} + +// func createRequest(data string, apiURL string) (req *http.Request, err error) +func TestCreateRequest(t *testing.T) { + data := "test" + apiURL := "http://localhost:8080/test" + + _, err := createRequest(data, apiURL) + if err != nil { + t.Error("Error occured, expected none") + } + + _, err = createRequest(data, brokenURL) + if err == nil { + t.Error("Expected error") + } + +} + +var brokenURL string = string([]byte{0x7f}) + +func TestSendRequest(t *testing.T) { + // Set up standard response & catch http requests + fakeBody := fmt.Sprint(`Test`) + + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(fakeBody)), + } + + // All ok! + newMockTransport(resp, false, nil) + apiURL := "http://localhost:8080/test" + s := fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload + req, _ := createRequest(s, apiURL) + err := sendRequest(req) + if err != nil { + t.Error("Expected no errors, error occured:", err) + } + + // Break defaultClient.Do() + + // Error unpacking body + resp.Body = errReader(0) + newMockTransport(resp, false, nil) + + err = sendRequest(req) + + if err == nil { + t.Error("Expected errors, no error occured:") + } + + // Error StatusCode + resp.Body = io.NopCloser(strings.NewReader(fakeBody)) + resp.StatusCode = 300 + newMockTransport(resp, false, nil) + err = sendRequest(req) + + if err != errStatusCode { + t.Error("Expected errStatusCode, got", err) + } + +} diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index e9d63cb..5cb0b59 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -1,363 +1,81 @@ package main import ( - "context" - "encoding/json" - "fmt" "io" "log" "net/http" + "net/http/httptest" "strings" "testing" + "time" - "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" ) -// mockTransport is used for replacing the default network Transport (used by -// http.DefaultClient) and it will intercept network requests. - -type mockTransport struct { - returnError bool - resp *http.Response - hits map[string]int - err error -} - -func newMockTransport(resp *http.Response, retErr bool, err error) mockTransport { - t := mockTransport{ - returnError: retErr, - resp: resp, - hits: make(map[string]int), - err: err, - } - // Highjack the default http client so no actuall http requests are sent over the network - http.DefaultClient.Transport = t - return t -} - -// TODO: this might need to be expanded to a full JSON array? - -const discoverExample string = `[{ - "Id": "123", - "Internalipaddress": "localhost", - "Macaddress": "test", - "Internalport": 8080, - "Name": "My gateway", - "Publicipaddress": "test" - }]` - -// RoundTrip method is required to fulfil the RoundTripper interface (as required by the DefaultClient). -// It prevents the request from being sent over the network and count how many times -// a domain was requested. - -var errHTTP error = fmt.Errorf("bad http request") - -func (t mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { - t.hits[req.URL.Hostname()] += 1 - if t.err != nil { - return nil, t.err - } - if t.returnError != false { - req.GetBody = func() (io.ReadCloser, error) { - return nil, errHTTP - } - } - t.resp.Request = req - return t.resp, nil -} - -//////////////////////////////////////////////////////////////////////////////// - -func TestUnitAsset(t *testing.T) { - // Create a form - f := forms.SignalA_v1a{ - Value: 27.0, - } - - // Creates a single UnitAsset and assert it changes - ua := initTemplate().(*UnitAsset) - - // Change Setpt - ua.setSetPoint(f) - - if ua.Setpt != 27.0 { - t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) - } - - // Fetch Setpt w/ a form - f2 := ua.getSetPoint() - - if f2.Value != f.Value { - t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) - } -} - -func TestGetters(t *testing.T) { +func TestSetpt(t *testing.T) { ua := initTemplate().(*UnitAsset) - name := ua.GetName() - if name != "Template" { - t.Errorf("Expected name to be 2, instead got %s", name) - } - - services := ua.GetServices() - if services == nil { - t.Fatalf("Expected services not to be nil") - } - if services["setpoint"].Definition != "setpoint" { - t.Errorf("Expected definition to be setpoint") - } - - details := ua.GetDetails() - if details == nil { - t.Fatalf("Details was nil, expected map") - } - if len(details["Location"]) == 0 { - t.Fatalf("Location was nil, expected kitchen") - } - - if details["Location"][0] != "Kitchen" { - t.Errorf("Expected location to be Kitchen") - } - - cervices := ua.GetCervices() - if cervices != nil { - t.Errorf("Expected no cervices") - } -} - -func TestNewResource(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sys := components.NewSystem("testsys", ctx) - - sys.Husk = &components.Husk{ - Description: " is a controller for smart thermostats connected with a RaspBee II", - Certificate: "ABCD", - Details: map[string][]string{"Developer": {"Arrowhead"}}, - ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, - InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", - } - - assetTemplate := initTemplate() - assetName := assetTemplate.GetName() - sys.UAssets[assetName] = &assetTemplate - - rawResources, servsTemp, err := usecases.Configure(&sys) - if err != nil { - log.Fatalf("Configuration error: %v\n", err) - } - - sys.UAssets = make(map[string]*components.UnitAsset) // clear the unit asset map (from the template) - for _, raw := range rawResources { - var uac UnitAsset - if err := json.Unmarshal(raw, &uac); err != nil { - log.Fatalf("Resource configuration error: %+v\n", err) - } - ua, _ := newResource(uac, &sys, servsTemp) - //startup() - sys.UAssets[ua.GetName()] = &ua - } -} - -type errReader int - -var errBodyRead error = fmt.Errorf("bad body read") - -func (errReader) Read(p []byte) (n int, err error) { - return 0, errBodyRead -} - -func (errReader) Close() error { - return nil -} - -func TestProcessFeedbackLoop(t *testing.T) { - // TODO: Test as much of the code as possible. - // Maybe try to pass arguments to processFeedbackLoop, to skip the GetState() function? - /* - fakeBody := // Find out what a form looks like, and pass it to this test function - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - newMockTransport(resp, false, nil) - - ua := initTemplate().(*UnitAsset) - ua.processFeedbackLoop() - */ -} - -func TestFindGateway(t *testing.T) { - fakeBody := fmt.Sprint(discoverExample) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - newMockTransport(resp, false, nil) - - // ---- All ok! ---- - err := findGateway() - - if err != nil { - t.Fatal("Gatewayn not found", err) - } - if gateway != "localhost:8080" { - t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) - } - - // ---- Error cases ---- - - // Unmarshall error - newMockTransport(resp, false, fmt.Errorf("Test error")) - err = findGateway() - if err == nil { - t.Error("Error expcted, got nil instead", err) - } - - // Statuscode > 299, have to make changes to mockTransport to test this - resp.StatusCode = 300 - newMockTransport(resp, false, nil) - err = findGateway() - if err != errStatusCode { - t.Error("Expected errStatusCode, got", err) - } - - // Broken body - https://stackoverflow.com/questions/45126312/how-do-i-test-an-error-on-reading-from-a-request-body - resp.StatusCode = 200 - resp.Body = errReader(0) - newMockTransport(resp, false, nil) - err = findGateway() - if err != errBodyRead { - t.Error("Expected error") - } - - // Actual http body is unmarshaled correctly - resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) - newMockTransport(resp, false, nil) - err = findGateway() - if err == nil { - t.Error("Expected error") - } - - // Empty list of gateways - resp.Body = io.NopCloser(strings.NewReader("[]")) - newMockTransport(resp, false, nil) - err = findGateway() - if err != errMissingGateway { - t.Error("Expected error", err) - } -} - -func TestToggleState(t *testing.T) { - fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - - newMockTransport(resp, false, nil) - ua := initTemplate().(*UnitAsset) - // All ok! - ua.toggleState(true) - // Error - // change gateway to bad character/url, return gateway to original value - gateway = brokenURL - ua.toggleState(true) - findGateway() -} - -func TestSendSetPoint(t *testing.T) { - fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - - newMockTransport(resp, false, nil) - ua := initTemplate().(*UnitAsset) - // All ok! - gateway = "" - err := ua.sendSetPoint() - if err != nil { - t.Error("Unexpected error:", err) - } - - // Error - gateway = brokenURL - ua.sendSetPoint() - findGateway() - -} - -// func createRequest(data string, apiURL string) (req *http.Request, err error) -func TestCreateRequest(t *testing.T) { - data := "test" - apiURL := "http://localhost:8080/test" - - _, err := createRequest(data, apiURL) - if err != nil { - t.Error("Error occured, expected none") - } - - _, err = createRequest(data, brokenURL) - if err == nil { - t.Error("Expected error") - } - -} - -var brokenURL string = string([]byte{0x7f}) - -func TestSendRequest(t *testing.T) { - // Set up standard response & catch http requests - fakeBody := fmt.Sprint(`Test`) - - resp := &http.Response{ - Status: "200 OK", - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(fakeBody)), - } - - // All ok! - newMockTransport(resp, false, nil) - apiURL := "http://localhost:8080/test" - s := fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload - req, _ := createRequest(s, apiURL) - err := sendRequest(req) - if err != nil { - t.Error("Expected no errors, error occured:", err) - } - - // Error unpacking body - resp.Body = errReader(0) - newMockTransport(resp, false, nil) - - err = sendRequest(req) - - if err == nil { - t.Error("Expected errors, no error occured:") - } - - // Error StatusCode - resp.Body = io.NopCloser(strings.NewReader(fakeBody)) - resp.StatusCode = 300 - newMockTransport(resp, false, nil) - err = sendRequest(req) - - if err != errStatusCode { - t.Error("Expected errStatusCode, got", err) + // Good case test: GET + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) + good_code := 200 + ua.setpt(w, r) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + stringBody := string(body) + // fmt.Println(stringBody) + + value := strings.Contains(string(stringBody), `"value": 20`) + unit := strings.Contains(string(stringBody), `"unit": "Celcius"`) + version := strings.Contains(string(stringBody), `"version": "SignalA_v1.0"`) + + if resp.StatusCode != good_code { + t.Errorf("Good GET: Expected good status code: %v, got %v", good_code, resp.StatusCode) + } + if value != true { + t.Errorf("Good GET: The statment to be true!") + } + if unit != true { + t.Errorf("Good GET: Expected the unit statement to be true!") + } + if version != true { + t.Errorf("Good GET: Expected the version statment to be true!") + } + // Bad test case: default part of code + w = httptest.NewRecorder() + r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) + + ua.setpt(w, r) + + resp = w.Result() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) + } + // ALL THE ABOVE PASSES TESTS + + // Good test case: PUT + // Send PUT request to change + w = httptest.NewRecorder() + // Make the body + var of forms.SignalA_v1a + of.NewForm() + of.Value = 25.0 + of.Unit = "Celcius" + of.Timestamp = time.Now() + op, _ := usecases.Pack(&of, "application/json") + log.Println(string(op)) + sentBody := io.NopCloser(strings.NewReader(string(op))) + // Send the request + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + ua.setpt(w, r) + resp = w.Result() + good_code = 200 + // Check for errors + if resp.StatusCode != good_code { + t.Errorf("Good PUT: Expected good status code: %v, got %v", good_code, resp.StatusCode) } } From af798aa5fb6ad35f4d1cb6e07b882cdde58dc963 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 3 Feb 2025 15:41:09 +0100 Subject: [PATCH 15/25] More tests, and fixed some problems with earlier tests --- ZigBeeValve/ZigBeeValve.go | 2 -- ZigBeeValve/thing_test.go | 1 - ZigBeeValve/zigbee_test.go | 65 ++++++++++++++++++++++++++++---------- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 148741f..a444282 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -93,7 +93,6 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { case "PUT": sig, err := usecases.HTTPProcessSetRequest(w, r) if err != nil { - log.Println("Error processing request") http.Error(w, "Request incorrectly formated", http.StatusBadRequest) return } @@ -102,7 +101,6 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { if rsc.Model == "SmartThermostat" { err = rsc.sendSetPoint() if err != nil { - log.Println("Error sending setpoint:") http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) return } diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index 0c6f684..d6e63aa 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -158,7 +158,6 @@ func TestNewResource(t *testing.T) { ua, _ := newResource(uac, &sys, nil) // Happy test case: - //Get a unitasset with correct values name := ua.GetName() if name != "Template" { t.Errorf("Expected name to be Template, but instead got: %v", name) diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 5cb0b59..084d444 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -1,16 +1,12 @@ package main import ( + "bytes" "io" - "log" "net/http" "net/http/httptest" "strings" "testing" - "time" - - "github.com/sdoque/mbaigo/forms" - "github.com/sdoque/mbaigo/usecases" ) func TestSetpt(t *testing.T) { @@ -54,28 +50,63 @@ func TestSetpt(t *testing.T) { if resp.StatusCode != http.StatusNotFound { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } - // ALL THE ABOVE PASSES TESTS - // Good test case: PUT - // Send PUT request to change + // Bad PUT (Cant reach ZigBee) w = httptest.NewRecorder() // Make the body - var of forms.SignalA_v1a - of.NewForm() - of.Value = 25.0 - of.Unit = "Celcius" - of.Timestamp = time.Now() - op, _ := usecases.Pack(&of, "application/json") - log.Println(string(op)) - sentBody := io.NopCloser(strings.NewReader(string(op))) + fakebody := string(`{"value": 24, "version": "SignalA_v1.0"}`) + sentBody := io.NopCloser(strings.NewReader(fakebody)) // Send the request r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) ua.setpt(w, r) resp = w.Result() good_code = 200 // Check for errors + if resp.StatusCode == good_code { + t.Errorf("Bad PUT: Expected bad status code: %v, got %v", good_code, resp.StatusCode) + } + + // Bad test case: PUT Failing @ HTTPProcessSetRequest + w = httptest.NewRecorder() + // Make the body + fakebody = string(`{"value": "24", "version": "SignalA_v1.0"}`) // MISSING VERSION IN SENTBODY + sentBody = io.NopCloser(strings.NewReader(fakebody)) + // Send the request + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + ua.setpt(w, r) + resp = w.Result() + // Check for errors + if resp.StatusCode == good_code { + t.Errorf("Bad PUT: Expected an error") + } + + // Good test case: PUT + w = httptest.NewRecorder() + // Make the body and request + fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) + sentBody = io.NopCloser(strings.NewReader(fakebody)) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + + // Mock the http response/traffic to zigbee + zBeeResponse := `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(zBeeResponse)), + } + newMockTransport(resp, false, nil) + // Set the response body to same as mock response + w.Body = bytes.NewBuffer([]byte(zBeeResponse)) + // Send the request + ua.setpt(w, r) + resp = w.Result() + // Check for errors if resp.StatusCode != good_code { t.Errorf("Good PUT: Expected good status code: %v, got %v", good_code, resp.StatusCode) } - + respBodyBytes, _ := io.ReadAll(resp.Body) + respBody := string(respBodyBytes) + if respBody != `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` { + t.Errorf("Wrong body") + } } From a7aed2cb12f5ed20c5200c036d417a546352fad9 Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 4 Feb 2025 11:14:17 +0100 Subject: [PATCH 16/25] Fixed some merge comments --- ZigBeeValve/thing.go | 33 +++++++++-------------------- ZigBeeValve/thing_test.go | 43 +++++++++++++------------------------- ZigBeeValve/zigbee_test.go | 19 +++++++---------- 3 files changed, 32 insertions(+), 63 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 2fc9689..05b8b67 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -95,7 +95,9 @@ func initTemplate() components.UnitAsset { //-------------------------------------Instatiate the unit assets based on configuration -// newResource creates the Resource resource with its pointers and channels based on the configuration using the tConig structs +// newResource creates the resource with its pointers and channels based on the configuration using the tConfig structs +// This is a startup function that's used to initiate the unit assets declared in the systemconfig.json, the function +// that is returned is later used to send a setpoint/start a goroutine depending on model of the unitasset func newResource(uac UnitAsset, sys *components.System, servs []components.Service) (components.UnitAsset, func()) { // deterimine the protocols that the system supports sProtocols := components.SProtocols(sys.Husk.ProtoPort) @@ -106,7 +108,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi Protos: sProtocols, Url: make([]string, 0), } - // intantiate the unit asset ua := &UnitAsset{ Name: uac.Name, @@ -121,25 +122,23 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi t.Name: t, }, } - var ref components.Service for _, s := range servs { if s.Definition == "setpoint" { ref = s } } - ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) - return ua, func() { if ua.Model == "SmartThermostat" { err := ua.sendSetPoint() if err != nil { log.Println("Error occured:", err) - // TODO: Turn off system if this startup() fails + // TODO: Turn off system if this startup() fails? } } else if ua.Model == "SmartPlug" { - // start the unit asset(s) + // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles + // between on/off depending on temperature in the room and a set temperature in the unitasset go ua.feedbackLoop(ua.Owner.Ctx) } } @@ -149,7 +148,6 @@ func (ua *UnitAsset) feedbackLoop(ctx context.Context) { // Initialize a ticker for periodic execution ticker := time.NewTicker(ua.Period * time.Second) defer ticker.Stop() - // start the control loop for { select { @@ -168,24 +166,22 @@ func (ua *UnitAsset) processFeedbackLoop() { log.Printf("\n unable to obtain a temperature reading error: %s\n", err) return } - // Perform a type assertion to convert the returned Form to SignalA_v1a tup, ok := tf.(*forms.SignalA_v1a) if !ok { log.Println("problem unpacking the temperature signal form") return } - // TODO: Check diff instead of a hard over/under value? meaning it'll only turn on/off if diff is over 0.5 degrees if tup.Value < ua.Setpt { err = ua.toggleState(true) if err != nil { - log.Println("Error occured: ", err) + log.Println("Error occured while toggling state to true: ", err) } } else { err = ua.toggleState(false) if err != nil { - log.Println("Error occured: ", err) + log.Println("Error occured while toggling state to false: ", err) } } } @@ -246,17 +242,11 @@ func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { // setSetPoint updates the thermal setpoint func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { ua.Setpt = f.Value - /* - log.Println("*---------------------*") - log.Printf("New set point: %.1f\n", f.Value) - log.Println("*---------------------*") - */ } func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" - // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload req, err := createRequest(s, apiURL) @@ -269,7 +259,6 @@ func (ua *UnitAsset) sendSetPoint() (err error) { func (ua *UnitAsset) toggleState(state bool) (err error) { // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" - // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload req, err := createRequest(s, apiURL) @@ -285,19 +274,17 @@ func createRequest(data string, apiURL string) (req *http.Request, err error) { if err != nil { return nil, err } - req.Header.Set("Content-Type", "application/json") // Make sure it's JSON return req, err } func sendRequest(req *http.Request) (err error) { - resp, err := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) // Perform the http request if err != nil { return err } - defer resp.Body.Close() - _, err = io.ReadAll(resp.Body) // Read the payload into body variable + _, err = io.ReadAll(resp.Body) // Read the response body, and check for errors/bad statuscodes if err != nil { return } diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index d6e63aa..f8b8956 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -72,20 +72,15 @@ func TestUnitAsset(t *testing.T) { f := forms.SignalA_v1a{ Value: 27.0, } - // Creates a single UnitAsset and assert it changes ua := initTemplate().(*UnitAsset) - // Change Setpt ua.setSetPoint(f) - if ua.Setpt != 27.0 { t.Errorf("Expected Setpt to be 27.0, instead got %f", ua.Setpt) } - // Fetch Setpt w/ a form f2 := ua.getSetPoint() - if f2.Value != f.Value { t.Errorf("Expected %f, instead got %f", f.Value, f2.Value) } @@ -93,12 +88,12 @@ func TestUnitAsset(t *testing.T) { func TestGetters(t *testing.T) { ua := initTemplate().(*UnitAsset) - + // Test GetName() name := ua.GetName() if name != "Template" { t.Errorf("Expected name to be 2, instead got %s", name) } - + // Test GetServices() services := ua.GetServices() if services == nil { t.Fatalf("Expected services not to be nil") @@ -106,7 +101,7 @@ func TestGetters(t *testing.T) { if services["setpoint"].Definition != "setpoint" { t.Errorf("Expected definition to be setpoint") } - + // Test GetDetails() details := ua.GetDetails() if details == nil { t.Fatalf("Details was nil, expected map") @@ -114,11 +109,10 @@ func TestGetters(t *testing.T) { if len(details["Location"]) == 0 { t.Fatalf("Location was nil, expected kitchen") } - if details["Location"][0] != "Kitchen" { t.Errorf("Expected location to be Kitchen") } - + // Test GetCervices() cervices := ua.GetCervices() if cervices != nil { t.Errorf("Expected no cervices") @@ -126,6 +120,7 @@ func TestGetters(t *testing.T) { } func TestNewResource(t *testing.T) { + // Setup test context, system and unitasset ctx, cancel := context.WithCancel(context.Background()) defer cancel() sys := components.NewSystem("testsys", ctx) @@ -136,14 +131,12 @@ func TestNewResource(t *testing.T) { ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, InfoLink: "https://github.com/sdoque/systems/tree/master/ZigBeeValve", } - setPointService := components.Service{ Definition: "setpoint", SubPath: "setpoint", Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current thermal setpoint (GET) or sets it (PUT)", } - uac := UnitAsset{ Name: "Template", Details: map[string][]string{"Location": {"Kitchen"}}, @@ -155,7 +148,7 @@ func TestNewResource(t *testing.T) { setPointService.SubPath: &setPointService, }, } - + // Test newResource function ua, _ := newResource(uac, &sys, nil) // Happy test case: name := ua.GetName() @@ -176,13 +169,9 @@ func (errReader) Close() error { return nil } -func TestProcessFeedbackLoop(t *testing.T) { - // TODO: Test as much of the code as possible? -} - func TestFindGateway(t *testing.T) { + // Create mock response for findGateway function fakeBody := fmt.Sprint(discoverExample) - resp := &http.Response{ Status: "200 OK", StatusCode: 200, @@ -192,21 +181,19 @@ func TestFindGateway(t *testing.T) { // ---- All ok! ---- err := findGateway() - if err != nil { - t.Fatal("Gatewayn not found", err) + t.Fatal("Gateway not found", err) } if gateway != "localhost:8080" { t.Fatalf("Expected gateway to be localhost:8080, was %s", gateway) } // ---- Error cases ---- - // Unmarshall error newMockTransport(resp, false, fmt.Errorf("Test error")) err = findGateway() if err == nil { - t.Error("Error expcted, got nil instead", err) + t.Error("Error expcted during unmarshalling, got nil instead", err) } // Statuscode > 299, have to make changes to mockTransport to test this @@ -223,7 +210,7 @@ func TestFindGateway(t *testing.T) { newMockTransport(resp, false, nil) err = findGateway() if err != errBodyRead { - t.Error("Expected error") + t.Error("Expected errBodyRead, got", err) } // Actual http body is unmarshaled correctly @@ -231,7 +218,7 @@ func TestFindGateway(t *testing.T) { newMockTransport(resp, false, nil) err = findGateway() if err == nil { - t.Error("Expected error") + t.Error("Expected error while unmarshalling body, error:", err) } // Empty list of gateways @@ -239,19 +226,18 @@ func TestFindGateway(t *testing.T) { newMockTransport(resp, false, nil) err = findGateway() if err != errMissingGateway { - t.Error("Expected error", err) + t.Error("Expected error when list of gateways is empty:", err) } } func TestToggleState(t *testing.T) { + // Create mock response and unitasset for toggleState() function fakeBody := fmt.Sprint(`{"on":true, "Version": "SignalA_v1a"}`) - resp := &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) // All ok! @@ -264,14 +250,13 @@ func TestToggleState(t *testing.T) { } func TestSendSetPoint(t *testing.T) { + // Create mock response and unitasset for sendSetPoint() function fakeBody := fmt.Sprint(`{"Value": 12.4, "Version": "SignalA_v1a}`) - resp := &http.Response{ Status: "200 OK", StatusCode: 200, Body: io.NopCloser(strings.NewReader(fakeBody)), } - newMockTransport(resp, false, nil) ua := initTemplate().(*UnitAsset) // All ok! diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 084d444..7f48353 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -17,11 +17,10 @@ func TestSetpt(t *testing.T) { r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) good_code := 200 ua.setpt(w, r) - + // Read response to a string, and save it in stringBody resp := w.Result() body, _ := io.ReadAll(resp.Body) stringBody := string(body) - // fmt.Println(stringBody) value := strings.Contains(string(stringBody), `"value": 20`) unit := strings.Contains(string(stringBody), `"unit": "Celcius"`) @@ -31,7 +30,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Good GET: Expected good status code: %v, got %v", good_code, resp.StatusCode) } if value != true { - t.Errorf("Good GET: The statment to be true!") + t.Errorf("Good GET: The value statment should be true!") } if unit != true { t.Errorf("Good GET: Expected the unit statement to be true!") @@ -39,14 +38,12 @@ func TestSetpt(t *testing.T) { if version != true { t.Errorf("Good GET: Expected the version statment to be true!") } - // Bad test case: default part of code + // Bad test case: Default part of code (faulty http method) w = httptest.NewRecorder() r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) - ua.setpt(w, r) - + // Read response and check statuscode, expecting 404 (StatusNotFound) resp = w.Result() - if resp.StatusCode != http.StatusNotFound { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } @@ -61,9 +58,9 @@ func TestSetpt(t *testing.T) { ua.setpt(w, r) resp = w.Result() good_code = 200 - // Check for errors + // Check for errors, should not be 200 if resp.StatusCode == good_code { - t.Errorf("Bad PUT: Expected bad status code: %v, got %v", good_code, resp.StatusCode) + t.Errorf("Bad PUT: Expected bad status code: got %v", resp.StatusCode) } // Bad test case: PUT Failing @ HTTPProcessSetRequest @@ -77,7 +74,7 @@ func TestSetpt(t *testing.T) { resp = w.Result() // Check for errors if resp.StatusCode == good_code { - t.Errorf("Bad PUT: Expected an error") + t.Errorf("Bad PUT: Expected an error during HTTPProcessSetRequest") } // Good test case: PUT @@ -86,7 +83,6 @@ func TestSetpt(t *testing.T) { fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) sentBody = io.NopCloser(strings.NewReader(fakebody)) r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) - // Mock the http response/traffic to zigbee zBeeResponse := `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` resp = &http.Response{ @@ -104,6 +100,7 @@ func TestSetpt(t *testing.T) { if resp.StatusCode != good_code { t.Errorf("Good PUT: Expected good status code: %v, got %v", good_code, resp.StatusCode) } + // Convert body to a string and check that it's correct respBodyBytes, _ := io.ReadAll(resp.Body) respBody := string(respBodyBytes) if respBody != `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` { From 7b0c41d2d954c43b335387e933ae5051b94e3127 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Feb 2025 17:44:40 +0100 Subject: [PATCH 17/25] Fixes spelling error on signal's unit --- ZigBeeValve/thing.go | 2 +- ZigBeeValve/zigbee_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 05b8b67..a52770a 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -234,7 +234,7 @@ func findGateway() (err error) { func (ua *UnitAsset) getSetPoint() (f forms.SignalA_v1a) { f.NewForm() f.Value = ua.Setpt - f.Unit = "Celcius" + f.Unit = "Celsius" f.Timestamp = time.Now() return f } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 7f48353..ab0f538 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -23,7 +23,7 @@ func TestSetpt(t *testing.T) { stringBody := string(body) value := strings.Contains(string(stringBody), `"value": 20`) - unit := strings.Contains(string(stringBody), `"unit": "Celcius"`) + unit := strings.Contains(string(stringBody), `"unit": "Celsius"`) version := strings.Contains(string(stringBody), `"version": "SignalA_v1.0"`) if resp.StatusCode != good_code { From 914ced526ac826111f7821d8360a9581b8d11ad1 Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 6 Feb 2025 10:04:17 +0100 Subject: [PATCH 18/25] changed a few comments --- ZigBeeValve/zigbee_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 7f48353..e3220eb 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -12,7 +12,7 @@ import ( func TestSetpt(t *testing.T) { ua := initTemplate().(*UnitAsset) - // Good case test: GET + // --- Good case test: GET --- w := httptest.NewRecorder() r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) good_code := 200 @@ -38,7 +38,7 @@ func TestSetpt(t *testing.T) { if version != true { t.Errorf("Good GET: Expected the version statment to be true!") } - // Bad test case: Default part of code (faulty http method) + // --- Bad test case: Default part of code (faulty http method) --- w = httptest.NewRecorder() r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) ua.setpt(w, r) @@ -48,7 +48,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Expected the status to be bad but got: %v", resp.StatusCode) } - // Bad PUT (Cant reach ZigBee) + // --- Bad PUT (Cant reach ZigBee) --- w = httptest.NewRecorder() // Make the body fakebody := string(`{"value": 24, "version": "SignalA_v1.0"}`) @@ -63,7 +63,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Bad PUT: Expected bad status code: got %v", resp.StatusCode) } - // Bad test case: PUT Failing @ HTTPProcessSetRequest + // --- Bad test case: PUT Failing @ HTTPProcessSetRequest --- w = httptest.NewRecorder() // Make the body fakebody = string(`{"value": "24", "version": "SignalA_v1.0"}`) // MISSING VERSION IN SENTBODY @@ -77,7 +77,7 @@ func TestSetpt(t *testing.T) { t.Errorf("Bad PUT: Expected an error during HTTPProcessSetRequest") } - // Good test case: PUT + // --- Good test case: PUT --- w = httptest.NewRecorder() // Make the body and request fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) From 245145ecda0c9e254ed27c17c90ff1bbb7dbf481 Mon Sep 17 00:00:00 2001 From: Pake Date: Mon, 10 Feb 2025 11:03:44 +0100 Subject: [PATCH 19/25] Added functions and other goodies --- ZigBeeValve/ZigBeeValve.go | 18 ++++----- ZigBeeValve/thing.go | 80 +++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index a444282..3a134d7 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -21,11 +21,11 @@ func main() { defer cancel() // make sure all paths cancel the context to avoid context leak // instantiate the System - sys := components.NewSystem("ZigBee", ctx) + sys := components.NewSystem("ZigBeeHandler", ctx) // Instatiate the Capusle sys.Husk = &components.Husk{ - Description: " is a controller for smart thermostats connected with a RaspBee II", + Description: " is a controller for smart devices connected with a RaspBee II", Certificate: "ABCD", Details: map[string][]string{"Developer": {"Arrowhead"}}, ProtoPort: map[string]int{"https": 0, "http": 8870, "coap": 0}, @@ -37,6 +37,12 @@ func main() { assetName := assetTemplate.GetName() sys.UAssets[assetName] = &assetTemplate + // Find zigbee gateway and store it in a global variable for reuse + err := findGateway() + if err != nil { + log.Fatal("Error getting gateway, shutting down: ", err) + } + // Configure the system rawResources, servsTemp, err := usecases.Configure(&sys) if err != nil { @@ -59,12 +65,6 @@ func main() { // Register the (system) and its services usecases.RegisterServices(&sys) - // Find zigbee gateway and store it in a global variable for reuse - err = findGateway() - if err != nil { - log.Fatal("Error getting gateway, shutting down: ", err) - } - // start the http handler and server go usecases.SetoutServers(&sys) @@ -98,7 +98,7 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { } rsc.setSetPoint(sig) - if rsc.Model == "SmartThermostat" { + if rsc.Model == "ZHAThermostat" { err = rsc.sendSetPoint() if err != nil { http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index a52770a..20310ce 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -38,10 +38,12 @@ type UnitAsset struct { ServicesMap components.Services `json:"-"` CervicesMap components.Cervices `json:"-"` // - Model string `json:"model"` - Period time.Duration `json:"period"` - Setpt float64 `json:"setpoint"` - Apikey string `json:"APIkey"` + Model string `json:"model"` + Uniqueid string `json:"uniqueid"` + deviceIndex string + Period time.Duration `json:"period"` + Setpt float64 `json:"setpoint"` + Apikey string `json:"APIkey"` } // GetName returns the name of the Resource. @@ -80,12 +82,14 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "Template", - Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "SmartThermostat", - Period: 10, - Setpt: 20, - Apikey: "1234", + Name: "Smart Thermostat 1", + Details: map[string][]string{"Location": {"Kitchen"}}, + Model: "SmartThermostat", + Uniqueid: "14:ef:14:10:00:6f:d0:d7-11-1201", + deviceIndex: "", + Period: 10, + Setpt: 20, + Apikey: "1234", ServicesMap: components.Services{ setPointService.SubPath: &setPointService, }, @@ -115,6 +119,8 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi Details: uac.Details, ServicesMap: components.CloneServices(servs), Model: uac.Model, + Uniqueid: uac.Uniqueid, + deviceIndex: uac.deviceIndex, Period: uac.Period, Setpt: uac.Setpt, Apikey: uac.Apikey, @@ -129,14 +135,18 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi } } ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) + return ua, func() { - if ua.Model == "SmartThermostat" { + + if ua.Model == "ZHAThermostat" { + ua.getConnectedUnits("sensors") err := ua.sendSetPoint() if err != nil { - log.Println("Error occured:", err) + log.Println("Error occured during startup, while calling sendSetPoint():", err) // TODO: Turn off system if this startup() fails? } - } else if ua.Model == "SmartPlug" { + } else if ua.Model == "Smart plug" { + ua.getConnectedUnits("lights") // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles // between on/off depending on temperature in the room and a set temperature in the unitasset go ua.feedbackLoop(ua.Owner.Ctx) @@ -192,6 +202,7 @@ const discoveryURL string = "https://phoscon.de/discover" var errStatusCode error = fmt.Errorf("bad status code") var errMissingGateway error = fmt.Errorf("missing gateway") +var errMissingUniqueID error = fmt.Errorf("uniqueid not found") func findGateway() (err error) { // https://pkg.go.dev/net/http#Get @@ -246,7 +257,9 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.Name + "/config" + + // --- Send setpoint to specific unit --- + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.deviceIndex + "/config" // Create http friendly payload s := fmt.Sprintf(`{"heatsetpoint":%f}`, ua.Setpt*100) // Create payload req, err := createRequest(s, apiURL) @@ -258,7 +271,7 @@ func (ua *UnitAsset) sendSetPoint() (err error) { func (ua *UnitAsset) toggleState(state bool) (err error) { // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config - apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.Name + "/state" + apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.deviceIndex + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload req, err := createRequest(s, apiURL) @@ -268,6 +281,40 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { return sendRequest(req) } +func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { + // Get all devices + apiURL := fmt.Sprintf("http://%s/api/%s/%s", gateway, ua.Apikey, unitType) + // Create a new request (Get) + // Put data into buffer + req, err := http.NewRequest(http.MethodGet, apiURL, nil) // Put request is made + req.Header.Set("Content-Type", "application/json") // Make sure it's JSON + // Send the request + resp, err := http.DefaultClient.Do(req) // Perform the http request + if err != nil { + return err + } + defer resp.Body.Close() + resBody, err := io.ReadAll(resp.Body) // Read the response body, and check for errors/bad statuscodes + if err != nil { + return + } + if resp.StatusCode > 299 { + return errStatusCode + } + + // How to access maps inside of maps below! + // https://stackoverflow.com/questions/28806951/accessing-nested-map-of-type-mapstringinterface-in-golang + var deviceMap map[string]interface{} + json.Unmarshal([]byte(resBody), &deviceMap) + for i := range deviceMap { + if deviceMap[i].(map[string]interface{})["uniqueid"] == ua.Uniqueid { + ua.deviceIndex = i + return + } + } + return errMissingUniqueID +} + func createRequest(data string, apiURL string) (req *http.Request, err error) { body := bytes.NewReader([]byte(data)) // Put data into buffer req, err = http.NewRequest(http.MethodPut, apiURL, body) // Put request is made @@ -293,3 +340,6 @@ func sendRequest(req *http.Request) (err error) { } return } + +// Create a group, add all lights/power plugs from e.g. kitchen to said group +// Create rule, on button.event toggle power plugs From f5b76cf129544e2f6611fb9b15b5e9a1ed421fe9 Mon Sep 17 00:00:00 2001 From: Pake Date: Tue, 11 Feb 2025 12:25:28 +0100 Subject: [PATCH 20/25] Added test for getConnectedUnits() --- ZigBeeValve/thing.go | 25 +++++--- ZigBeeValve/thing_test.go | 124 ++++++++++++++++++++++++++++++++++--- ZigBeeValve/zigbee_test.go | 34 ++++++---- 3 files changed, 154 insertions(+), 29 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 20310ce..0c78897 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -82,9 +82,9 @@ func initTemplate() components.UnitAsset { // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ - Name: "Smart Thermostat 1", + Name: "SmartThermostat1", Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "SmartThermostat", + Model: "ZHAThermostat", Uniqueid: "14:ef:14:10:00:6f:d0:d7-11-1201", deviceIndex: "", Period: 10, @@ -139,14 +139,22 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi return ua, func() { if ua.Model == "ZHAThermostat" { - ua.getConnectedUnits("sensors") - err := ua.sendSetPoint() + // Get correct index in list returned by api/sensors to make sure we always change correct device + err := ua.getConnectedUnits("sensors") + if err != nil { + log.Println("Error occured during startup, while calling getConnectedUnits:", err) + } + err = ua.sendSetPoint() if err != nil { log.Println("Error occured during startup, while calling sendSetPoint():", err) // TODO: Turn off system if this startup() fails? } } else if ua.Model == "Smart plug" { - ua.getConnectedUnits("lights") + // Get correct index in list returned by api/lights to make sure we always change correct device + err := ua.getConnectedUnits("lights") + if err != nil { + log.Println("Error occured during startup, while calling getConnectedUnits:", err) + } // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles // between on/off depending on temperature in the room and a set temperature in the unitasset go ua.feedbackLoop(ua.Owner.Ctx) @@ -229,13 +237,11 @@ func findGateway() (err error) { } if len(gw) < 1 { - //log.Println("No gateway was found") return errMissingGateway } // Save the gateway s := fmt.Sprintf(`%s:%d`, gw[0].Internalipaddress, gw[0].Internalport) gateway = s - //log.Println("Gateway found:", s) return } @@ -305,7 +311,10 @@ func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { // How to access maps inside of maps below! // https://stackoverflow.com/questions/28806951/accessing-nested-map-of-type-mapstringinterface-in-golang var deviceMap map[string]interface{} - json.Unmarshal([]byte(resBody), &deviceMap) + err = json.Unmarshal([]byte(resBody), &deviceMap) + if err != nil { + return + } for i := range deviceMap { if deviceMap[i].(map[string]interface{})["uniqueid"] == ua.Uniqueid { ua.deviceIndex = i diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index f8b8956..3063f6d 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -90,8 +91,8 @@ func TestGetters(t *testing.T) { ua := initTemplate().(*UnitAsset) // Test GetName() name := ua.GetName() - if name != "Template" { - t.Errorf("Expected name to be 2, instead got %s", name) + if name != "SmartThermostat1" { + t.Errorf("Expected name to be SmartThermostat1, instead got %s", name) } // Test GetServices() services := ua.GetServices() @@ -138,9 +139,9 @@ func TestNewResource(t *testing.T) { Description: "provides the current thermal setpoint (GET) or sets it (PUT)", } uac := UnitAsset{ - Name: "Template", + Name: "SmartThermostat1", Details: map[string][]string{"Location": {"Kitchen"}}, - Model: "SmartThermostat", + Model: "ZHAThermostat", Period: 10, Setpt: 20, Apikey: "1234", @@ -152,8 +153,8 @@ func TestNewResource(t *testing.T) { ua, _ := newResource(uac, &sys, nil) // Happy test case: name := ua.GetName() - if name != "Template" { - t.Errorf("Expected name to be Template, but instead got: %v", name) + if name != "SmartThermostat1" { + t.Errorf("Expected name to be SmartThermostat1, but instead got: %v", name) } } @@ -213,7 +214,7 @@ func TestFindGateway(t *testing.T) { t.Error("Expected errBodyRead, got", err) } - // Actual http body is unmarshaled correctly + // Actual http body is unmarshaled incorrectly resp.Body = io.NopCloser(strings.NewReader(fakeBody + "123")) newMockTransport(resp, false, nil) err = findGateway() @@ -273,6 +274,106 @@ func TestSendSetPoint(t *testing.T) { gateway = "localhost" } +type testJSON struct { + FirstAttr string `json:"firstAttr"` + Uniqueid string `json:"uniqueid"` + ThirdAttr string `json:"thirdAttr"` +} + +func TestGetConnectedUnits(t *testing.T) { + gateway = "localhost" + // Set up standard response & catch http requests + resp := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: nil, + } + ua := initTemplate().(*UnitAsset) + ua.Uniqueid = "123test" + + // --- Broken body --- + newMockTransport(resp, false, nil) + resp.Body = errReader(0) + err := ua.getConnectedUnits(ua.Model) + + if err == nil { + t.Error("Expected error while unpacking body in getConnectedUnits()") + } + + // --- All ok! --- + // Make a map + fakeBody := make(map[string]testJSON) + test := testJSON{ + FirstAttr: "123", + Uniqueid: "123test", + ThirdAttr: "456", + } + // Insert the JSON into the map with key="1" + fakeBody["1"] = test + // Marshal and create response + jsonBody, _ := json.Marshal(fakeBody) + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(string(jsonBody))), + } + // Start up a newMockTransport to capture HTTP requests before they leave + newMockTransport(resp, false, nil) + // Test function + err = ua.getConnectedUnits(ua.Model) + if err != nil { + t.Error("Expected no errors, error occured:", err) + } + + // --- Bad statuscode --- + newMockTransport(resp, false, nil) + resp.StatusCode = 300 + err = ua.getConnectedUnits(ua.Model) + if err == nil { + t.Errorf("Expected status code > 299 in getConnectedUnits(), got %v", resp.StatusCode) + } + + // --- Missing uniqueid --- + // Make a map + fakeBody = make(map[string]testJSON) + test = testJSON{ + FirstAttr: "123", + Uniqueid: "missing", + ThirdAttr: "456", + } + // Insert the JSON into the map with key="1" + fakeBody["1"] = test + // Marshal and create response + jsonBody, _ = json.Marshal(fakeBody) + resp = &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(string(jsonBody))), + } + // Start up a newMockTransport to capture HTTP requests before they leave + newMockTransport(resp, false, nil) + // Test function + err = ua.getConnectedUnits(ua.Model) + if err != errMissingUniqueID { + t.Error("Expected uniqueid to be missing when running getConnectedUnits()") + } + + // --- Unmarshall error --- + resp.Body = io.NopCloser(strings.NewReader(string(jsonBody) + "123")) + newMockTransport(resp, false, nil) + err = ua.getConnectedUnits(ua.Model) + if err == nil { + t.Error("Error expected during unmarshalling, got nil instead", err) + } + + // --- Error performing request --- + newMockTransport(resp, false, fmt.Errorf("Test error")) + err = ua.getConnectedUnits(ua.Model) + if err == nil { + t.Error("Error expected while performing http request, got nil instead") + } +} + // func createRequest(data string, apiURL string) (req *http.Request, err error) func TestCreateRequest(t *testing.T) { data := "test" @@ -313,6 +414,14 @@ func TestSendRequest(t *testing.T) { } // Break defaultClient.Do() + // --- Error performing request --- + newMockTransport(resp, false, fmt.Errorf("Test error")) + s = fmt.Sprintf(`{"heatsetpoint":%f}`, 25.0) // Create payload + req, _ = createRequest(s, apiURL) + err = sendRequest(req) + if err == nil { + t.Error("Error expected while performing http request, got nil instead") + } // Error unpacking body resp.Body = errReader(0) @@ -329,7 +438,6 @@ func TestSendRequest(t *testing.T) { resp.StatusCode = 300 newMockTransport(resp, false, nil) err = sendRequest(req) - if err != errStatusCode { t.Error("Expected errStatusCode, got", err) } diff --git a/ZigBeeValve/zigbee_test.go b/ZigBeeValve/zigbee_test.go index 515d474..ef08958 100644 --- a/ZigBeeValve/zigbee_test.go +++ b/ZigBeeValve/zigbee_test.go @@ -11,24 +11,27 @@ import ( func TestSetpt(t *testing.T) { ua := initTemplate().(*UnitAsset) + gateway = "localhost" + ua.deviceIndex = "1" // --- Good case test: GET --- w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/Template/setpoint", nil) + r := httptest.NewRequest("GET", "http://localhost:8670/ZigBee/SmartThermostat1/setpoint", nil) + r.Header.Set("Content-Type", "application/json") good_code := 200 ua.setpt(w, r) // Read response to a string, and save it in stringBody resp := w.Result() + if resp.StatusCode != good_code { + t.Errorf("expected good status code: %v, got %v", good_code, resp.StatusCode) + } body, _ := io.ReadAll(resp.Body) stringBody := string(body) - + // Check if correct values are present in the body, each line returns true/false value := strings.Contains(string(stringBody), `"value": 20`) unit := strings.Contains(string(stringBody), `"unit": "Celsius"`) version := strings.Contains(string(stringBody), `"version": "SignalA_v1.0"`) - - if resp.StatusCode != good_code { - t.Errorf("Good GET: Expected good status code: %v, got %v", good_code, resp.StatusCode) - } + // Check that above statements are true if value != true { t.Errorf("Good GET: The value statment should be true!") } @@ -38,9 +41,11 @@ func TestSetpt(t *testing.T) { if version != true { t.Errorf("Good GET: Expected the version statment to be true!") } + // --- Bad test case: Default part of code (faulty http method) --- w = httptest.NewRecorder() - r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/Template/setpoint", nil) + r = httptest.NewRequest("123", "http://localhost:8670/ZigBee/SmartThermostat1/setpoint", nil) + r.Header.Set("Content-Type", "application/json") ua.setpt(w, r) // Read response and check statuscode, expecting 404 (StatusNotFound) resp = w.Result() @@ -54,22 +59,24 @@ func TestSetpt(t *testing.T) { fakebody := string(`{"value": 24, "version": "SignalA_v1.0"}`) sentBody := io.NopCloser(strings.NewReader(fakebody)) // Send the request - r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/SmartThermostat1/setpoint", sentBody) + r.Header.Set("Content-Type", "application/json") ua.setpt(w, r) resp = w.Result() - good_code = 200 + resp.StatusCode = 404 // Simulate zigbee gateway not found? // Check for errors, should not be 200 if resp.StatusCode == good_code { - t.Errorf("Bad PUT: Expected bad status code: got %v", resp.StatusCode) + t.Errorf("Bad PUT: Expected bad status code: got %v.", resp.StatusCode) } // --- Bad test case: PUT Failing @ HTTPProcessSetRequest --- w = httptest.NewRecorder() // Make the body - fakebody = string(`{"value": "24", "version": "SignalA_v1.0"}`) // MISSING VERSION IN SENTBODY + fakebody = string(`{"value": "24"`) // MISSING VERSION IN SENTBODY sentBody = io.NopCloser(strings.NewReader(fakebody)) // Send the request - r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/SmartThermostat1/setpoint", sentBody) + r.Header.Set("Content-Type", "application/json") ua.setpt(w, r) resp = w.Result() // Check for errors @@ -82,7 +89,8 @@ func TestSetpt(t *testing.T) { // Make the body and request fakebody = string(`{"value": 24, "version": "SignalA_v1.0"}`) sentBody = io.NopCloser(strings.NewReader(fakebody)) - r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/Template/setpoint", sentBody) + r = httptest.NewRequest("PUT", "http://localhost:8870/ZigBee/SmartThermostat1/setpoint", sentBody) + r.Header.Set("Content-Type", "application/json") // Mock the http response/traffic to zigbee zBeeResponse := `[{"success":{"/sensors/7/config/heatsetpoint":2400}}]` resp = &http.Response{ From 9a9cbae6596afd57b7fdd35b78c0567a9ec3f124 Mon Sep 17 00:00:00 2001 From: Pake Date: Wed, 12 Feb 2025 19:17:46 +0100 Subject: [PATCH 21/25] Fixed some PR comments --- ZigBeeValve/ZigBeeValve.go | 4 ++-- ZigBeeValve/thing.go | 29 ++++++++++++++++++----------- ZigBeeValve/thing_test.go | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/ZigBeeValve/ZigBeeValve.go b/ZigBeeValve/ZigBeeValve.go index 3a134d7..91d0d5b 100644 --- a/ZigBeeValve/ZigBeeValve.go +++ b/ZigBeeValve/ZigBeeValve.go @@ -81,7 +81,7 @@ func (t *UnitAsset) Serving(w http.ResponseWriter, r *http.Request, servicePath case "setpoint": t.setpt(w, r) default: - http.Error(w, "Invalid service request [Do not modify the services subpath in the configurration file]", http.StatusBadRequest) + http.Error(w, "Invalid service request [Do not modify the services subpath in the configuration file]", http.StatusBadRequest) } } @@ -101,11 +101,11 @@ func (rsc *UnitAsset) setpt(w http.ResponseWriter, r *http.Request) { if rsc.Model == "ZHAThermostat" { err = rsc.sendSetPoint() if err != nil { + log.Println("Error sending setpoint:", err) http.Error(w, "Couldn't send setpoint.", http.StatusInternalServerError) return } } - default: http.Error(w, "Method is not supported.", http.StatusNotFound) } diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 0c78897..016fd74 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -79,7 +79,14 @@ func initTemplate() components.UnitAsset { Details: map[string][]string{"Unit": {"Celsius"}, "Forms": {"SignalA_v1a"}}, Description: "provides the current thermal setpoint (GET) or sets it (PUT)", } - + /* + consumptionService := components.Service{ + Definition: "consumption", + SubPath: "consumption", + Details: map[string][]string{"Unit": {"Wh"}, "Forms": {"SignalA_v1a"}}, + Description: "provides the current consumption of the device (GET)", + } + */ // var uat components.UnitAsset // this is an interface, which we then initialize uat := &UnitAsset{ Name: "SmartThermostat1", @@ -137,7 +144,6 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi ua.CervicesMap["temperature"].Details = components.MergeDetails(ua.Details, ref.Details) return ua, func() { - if ua.Model == "ZHAThermostat" { // Get correct index in list returned by api/sensors to make sure we always change correct device err := ua.getConnectedUnits("sensors") @@ -155,9 +161,12 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi if err != nil { log.Println("Error occured during startup, while calling getConnectedUnits:", err) } - // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles - // between on/off depending on temperature in the room and a set temperature in the unitasset - go ua.feedbackLoop(ua.Owner.Ctx) + // Not all smart plugs should be handled by the feedbackloop, some should be handled by a switch + if ua.Period != 0 { + // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles + // between on/off depending on temperature in the room and a set temperature in the unitasset + go ua.feedbackLoop(ua.Owner.Ctx) + } } } } @@ -223,7 +232,6 @@ func findGateway() (err error) { } defer res.Body.Close() if res.StatusCode > 299 { - //log.Printf("Response failed with status code: %d and\n", res.StatusCode) return errStatusCode } body, err := io.ReadAll(res.Body) // Read the payload into body variable @@ -235,7 +243,7 @@ func findGateway() (err error) { if err != nil { return } - + // If the returned list is empty, return a missing gateway error if len(gw) < 1 { return errMissingGateway } @@ -263,7 +271,6 @@ func (ua *UnitAsset) setSetPoint(f forms.SignalA_v1a) { func (ua *UnitAsset) sendSetPoint() (err error) { // API call to set desired temp in smart thermostat, PUT call should be sent to URL/api/apikey/sensors/sensor_id/config - // --- Send setpoint to specific unit --- apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/sensors/" + ua.deviceIndex + "/config" // Create http friendly payload @@ -276,7 +283,7 @@ func (ua *UnitAsset) sendSetPoint() (err error) { } func (ua *UnitAsset) toggleState(state bool) (err error) { - // API call turn smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config + // API call to toggle smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config apiURL := "http://" + gateway + "/api/" + ua.Apikey + "/lights/" + ua.deviceIndex + "/state" // Create http friendly payload s := fmt.Sprintf(`{"on":%t}`, state) // Create payload @@ -288,7 +295,7 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { } func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { - // Get all devices + // --- Get all devices --- apiURL := fmt.Sprintf("http://%s/api/%s/%s", gateway, ua.Apikey, unitType) // Create a new request (Get) // Put data into buffer @@ -307,7 +314,6 @@ func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { if resp.StatusCode > 299 { return errStatusCode } - // How to access maps inside of maps below! // https://stackoverflow.com/questions/28806951/accessing-nested-map-of-type-mapstringinterface-in-golang var deviceMap map[string]interface{} @@ -315,6 +321,7 @@ func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { if err != nil { return } + // --- Find the index of a device with the specific UniqueID --- for i := range deviceMap { if deviceMap[i].(map[string]interface{})["uniqueid"] == ua.Uniqueid { ua.deviceIndex = i diff --git a/ZigBeeValve/thing_test.go b/ZigBeeValve/thing_test.go index 3063f6d..e4f6aab 100644 --- a/ZigBeeValve/thing_test.go +++ b/ZigBeeValve/thing_test.go @@ -326,8 +326,8 @@ func TestGetConnectedUnits(t *testing.T) { } // --- Bad statuscode --- - newMockTransport(resp, false, nil) resp.StatusCode = 300 + newMockTransport(resp, false, nil) err = ua.getConnectedUnits(ua.Model) if err == nil { t.Errorf("Expected status code > 299 in getConnectedUnits(), got %v", resp.StatusCode) From 8f869d222ad458d57c3e7f60fde5b4baa5c4be78 Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 13 Feb 2025 03:49:13 +0100 Subject: [PATCH 22/25] Added code for a WebsocketClient, needs to be tested on Raspberry Pi --- ZigBeeValve/thing.go | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 016fd74..1c5969c 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -13,6 +13,8 @@ import ( "net/http" "time" + "github.com/coder/websocket" + // "github.com/coder/websocket/wsjson" "github.com/sdoque/mbaigo/components" "github.com/sdoque/mbaigo/forms" "github.com/sdoque/mbaigo/usecases" @@ -357,5 +359,37 @@ func sendRequest(req *http.Request) (err error) { return } -// Create a group, add all lights/power plugs from e.g. kitchen to said group -// Create rule, on button.event toggle power plugs +// --- HOW TO CONNECT AND LISTEN TO A WEBSOCKET --- +// Port 443, can be found by curl -v "http://localhost:8080/api/[apikey]/config", and getting the "websocketport". Will make a function to automatically get this port +// https://stackoverflow.com/questions/32745716/i-need-to-connect-to-an-existing-websocket-server-using-go-lang +// https://pkg.go.dev/github.com/coder/websocket#Conn +// https://pkg.go.dev/github.com/coder/websocket#Conn.Read + +// Not sure if this will work, still a work in progress. +func initWebsocketClient(ctx context.Context) (err error) { + fmt.Println("Starting Client") + ws, _, err := websocket.Dial(ctx, "ws://localhost:443", nil) // Start listening to websocket + defer ws.CloseNow() // Make sure connection is closed when returning from function + if err != nil { + fmt.Printf("Dial failed: %s\n", err) + return err + } + _, body, err := ws.Reader(ctx) // Start reading from connection, returned body will be used to get buttonevents + if err != nil { + log.Println("Error while reading from websocket:", err) + return + } + data, err := io.ReadAll(body) + if err != nil { + log.Println("Error while converthing from io.Reader to []byte:", err) + return + } + var bodyString map[string]interface{} + err = json.Unmarshal(data, &bodyString) // Unmarshal body into json, easier to be able to point to specific data with ".example" + if err != nil { + log.Println("Error while unmarshaling data:", err) + return + } + log.Println("Read from websocket:", bodyString) + return +} From 75522b07eb3f75a9ceba060d904ee5ce0ea1bcab Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 13 Feb 2025 03:51:31 +0100 Subject: [PATCH 23/25] Forgot to add go.mod & go.sum files --- go.mod | 2 ++ go.sum | 2 ++ 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index 82ef070..a6e1cec 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/lmas/d0020e_code go 1.23 require github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 + +require github.com/coder/websocket v1.8.12 // indirect diff --git a/go.sum b/go.sum index 0f4b1d6..668ac3a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4 h1:feRW3hSquROFeId8H0ZEUsH/kEzd4AAVxjsYkQd1cCs= github.com/sdoque/mbaigo v0.0.0-20241019053937-4e5abf6a2df4/go.mod h1:Bfx9Uj0uiTT7BCzzlImMiRd6vMoPQdsZIHGMQOVjx80= From ebbb495d4a3221a8183619a792eb584eb29600ca Mon Sep 17 00:00:00 2001 From: Pake Date: Thu, 13 Feb 2025 03:58:41 +0100 Subject: [PATCH 24/25] Forgot to add a link to useful info, and a line of code to properly close connection --- ZigBeeValve/thing.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 1c5969c..2e15563 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -361,6 +361,7 @@ func sendRequest(req *http.Request) (err error) { // --- HOW TO CONNECT AND LISTEN TO A WEBSOCKET --- // Port 443, can be found by curl -v "http://localhost:8080/api/[apikey]/config", and getting the "websocketport". Will make a function to automatically get this port +// https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/ // https://stackoverflow.com/questions/32745716/i-need-to-connect-to-an-existing-websocket-server-using-go-lang // https://pkg.go.dev/github.com/coder/websocket#Conn // https://pkg.go.dev/github.com/coder/websocket#Conn.Read @@ -391,5 +392,6 @@ func initWebsocketClient(ctx context.Context) (err error) { return } log.Println("Read from websocket:", bodyString) + ws.Close(websocket.StatusNormalClosure, "No longer need to listen to websocket") return } From 51621034122fe2ce05f711258cf343fd3f5e06f4 Mon Sep 17 00:00:00 2001 From: Pake Date: Fri, 14 Feb 2025 02:33:44 +0100 Subject: [PATCH 25/25] Added an error message to pass linter in example code for listening to websocket --- ZigBeeValve/thing.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ZigBeeValve/thing.go b/ZigBeeValve/thing.go index 2e15563..59c337c 100644 --- a/ZigBeeValve/thing.go +++ b/ZigBeeValve/thing.go @@ -296,6 +296,8 @@ func (ua *UnitAsset) toggleState(state bool) (err error) { return sendRequest(req) } +// Useless function? Noticed uniqueid can be used as "id" to send requests instead of the index while testing, wasn't clear from documentation. Will need to test this more though +// TODO: Rewrite this to instead get the websocketport. func (ua *UnitAsset) getConnectedUnits(unitType string) (err error) { // --- Get all devices --- apiURL := fmt.Sprintf("http://%s/api/%s/%s", gateway, ua.Apikey, unitType) @@ -363,8 +365,8 @@ func sendRequest(req *http.Request) (err error) { // Port 443, can be found by curl -v "http://localhost:8080/api/[apikey]/config", and getting the "websocketport". Will make a function to automatically get this port // https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/ // https://stackoverflow.com/questions/32745716/i-need-to-connect-to-an-existing-websocket-server-using-go-lang -// https://pkg.go.dev/github.com/coder/websocket#Conn -// https://pkg.go.dev/github.com/coder/websocket#Conn.Read +// https://pkg.go.dev/github.com/coder/websocket#Dial +// https://pkg.go.dev/github.com/coder/websocket#Conn.Reader // Not sure if this will work, still a work in progress. func initWebsocketClient(ctx context.Context) (err error) { @@ -392,6 +394,11 @@ func initWebsocketClient(ctx context.Context) (err error) { return } log.Println("Read from websocket:", bodyString) - ws.Close(websocket.StatusNormalClosure, "No longer need to listen to websocket") + err = ws.Close(websocket.StatusNormalClosure, "No longer need to listen to websocket") + if err != nil { + log.Println("Error while doing normal closure on websocket") + return + } return + // Have to do something fancy to make sure we update "connected" plugs/lights when Reader returns a body actually containing a buttonevent (something w/ channels?) }