diff --git a/charger/homewizard.go b/charger/homewizard.go new file mode 100644 index 000000000..16c1f6043 --- /dev/null +++ b/charger/homewizard.go @@ -0,0 +1,100 @@ +package charger + +import ( + "errors" + "fmt" + "net/http" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/homewizard" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +// HomeWizard project homepage +// https://homewizard-energy-api.readthedocs.io/index.html + +// HomeWizard charger implementation +type HomeWizard struct { + conn *homewizard.Connection + *switchSocket +} + +func init() { + registry.Add("homewizard", NewHomeWizardFromConfig) +} + +// NewHomeWizardFromConfig creates a HomeWizard charger from generic config +func NewHomeWizardFromConfig(other map[string]interface{}) (api.Charger, error) { + var cc = struct { + embed `mapstructure:",squash"` + URI string + StandbyPower float64 + }{} + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewHomeWizard(cc.embed, cc.URI, cc.StandbyPower) +} + +// NewHomeWizard creates HomeWizard charger +func NewHomeWizard(embed embed, uri string, standbypower float64) (*HomeWizard, error) { + conn, err := homewizard.NewConnection(uri) + if err != nil { + return nil, err + } + + c := &HomeWizard{ + conn: conn, + } + + // Check compatible product type + if c.conn.ProductType != "HWE-SKT" { + return nil, errors.New("not supported product type: " + c.conn.ProductType) + } + + c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower) + + return c, nil +} + +// Enabled implements the api.Charger interface +func (c *HomeWizard) Enabled() (bool, error) { + var res homewizard.StateResponse + err := c.conn.GetJSON(fmt.Sprintf("%s/data", c.conn.URI), &res) + return res.PowerOn, err +} + +// Enable implements the api.Charger interface +func (c *HomeWizard) Enable(enable bool) error { + var res homewizard.StateResponse + data := map[string]interface{}{ + "power_on": enable, + } + + req, err := request.New(http.MethodPut, fmt.Sprintf("%s/state", c.conn.URI), request.MarshalJSON(data), request.JSONEncoding) + if err != nil { + return err + } + if err := c.conn.DoJSON(req, &res); err != nil { + return err + } + + switch { + case enable && !res.PowerOn: + return errors.New("switchOn failed") + case !enable && res.PowerOn: + return errors.New("switchOff failed") + default: + return nil + } +} + +var _ api.MeterEnergy = (*HomeWizard)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (c *HomeWizard) TotalEnergy() (float64, error) { + return c.conn.TotalEnergy() +} diff --git a/meter/homewizard.go b/meter/homewizard.go new file mode 100644 index 000000000..9c354d375 --- /dev/null +++ b/meter/homewizard.go @@ -0,0 +1,58 @@ +package meter + +import ( + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/homewizard" + "github.com/evcc-io/evcc/util" +) + +// HomeWizard meter implementation +type HomeWizard struct { + conn *homewizard.Connection +} + +// HomeWizard meter implementation +func init() { + registry.Add("homewizard", NewHomeWizardFromConfig) +} + +// NewHomeWizardFromConfig creates a HomeWizard meter from generic config +func NewHomeWizardFromConfig(other map[string]interface{}) (api.Meter, error) { + cc := struct { + URI string + }{} + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewHomeWizard(cc.URI) +} + +// NewHomeWizard creates HomeWizard meter +func NewHomeWizard(uri string) (*HomeWizard, error) { + conn, err := homewizard.NewConnection(uri) + if err != nil { + return nil, err + } + + c := &HomeWizard{ + conn: conn, + } + + return c, err +} + +var _ api.Meter = (*HomeWizard)(nil) + +// CurrentPower implements the api.Meter interface +func (c *HomeWizard) CurrentPower() (float64, error) { + return c.conn.CurrentPower() +} + +var _ api.MeterEnergy = (*HomeWizard)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (c *HomeWizard) TotalEnergy() (float64, error) { + return c.conn.TotalEnergy() +} diff --git a/meter/homewizard/connection.go b/meter/homewizard/connection.go new file mode 100644 index 000000000..4e0a8bbc6 --- /dev/null +++ b/meter/homewizard/connection.go @@ -0,0 +1,61 @@ +package homewizard + +import ( + "errors" + "fmt" + "strings" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" +) + +// Connection is the homewizard connection +type Connection struct { + *request.Helper + URI string + ProductType string +} + +// NewConnection creates a homewizard connection +func NewConnection(uri string) (*Connection, error) { + if uri == "" { + return nil, errors.New("missing uri") + } + + log := util.NewLogger("homewizard") + c := &Connection{ + Helper: request.NewHelper(log), + URI: fmt.Sprintf("%s/api", util.DefaultScheme(strings.TrimRight(uri, "/"), "http")), + } + + c.Client.Transport = request.NewTripper(log, transport.Insecure()) + + // Check and set API version + product type + var res ApiResponse + if err := c.GetJSON(c.URI, &res); err != nil { + return c, err + } + if res.ApiVersion != "v1" { + return nil, errors.New("not supported api version: " + res.ApiVersion) + } + + c.URI = c.URI + "/" + res.ApiVersion + c.ProductType = res.ProductType + + return c, nil +} + +// CurrentPower implements the api.Meter interface +func (d *Connection) CurrentPower() (float64, error) { + var res DataResponse + err := d.GetJSON(fmt.Sprintf("%s/data", d.URI), &res) + return res.ActivePowerW, err +} + +// TotalEnergy implements the api.MeterEnergy interface +func (d *Connection) TotalEnergy() (float64, error) { + var res DataResponse + err := d.GetJSON(fmt.Sprintf("%s/data", d.URI), &res) + return res.TotalPowerImportT1kWh + res.TotalPowerImportT2kWh + res.TotalPowerImportT3kWh + res.TotalPowerImportT4kWh, err +} diff --git a/meter/homewizard/types.go b/meter/homewizard/types.go new file mode 100644 index 000000000..a0039fa7b --- /dev/null +++ b/meter/homewizard/types.go @@ -0,0 +1,24 @@ +package homewizard + +// ApiResponse returns allows you to get basic information from the HomeWizard Energy Socket +// https://homewizard-energy-api.readthedocs.io/endpoints.html#basic-information-api +type ApiResponse struct { + ProductType string `json:"product_type"` + ApiVersion string `json:"api_version"` +} + +// StateResponse returns the actual state of the HomeWizard Energy Socket +// https://homewizard-energy-api.readthedocs.io/endpoints.html#recent-measurement-api-v1-data +type StateResponse struct { + PowerOn bool `json:"power_on"` +} + +// DataResponse returns the most recent measurements from the HomeWizard device +// https://homewizard-energy-api.readthedocs.io/endpoints.html#state-api-v1-state +type DataResponse struct { + ActivePowerW float64 `json:"active_power_w"` + TotalPowerImportT1kWh float64 `json:"total_power_import_t1_kwh"` + TotalPowerImportT2kWh float64 `json:"total_power_import_t2_kwh"` + TotalPowerImportT3kWh float64 `json:"total_power_import_t3_kwh"` + TotalPowerImportT4kWh float64 `json:"total_power_import_t4_kwh"` +} diff --git a/meter/homewizard/types_test.go b/meter/homewizard/types_test.go new file mode 100644 index 000000000..f9e548e66 --- /dev/null +++ b/meter/homewizard/types_test.go @@ -0,0 +1,46 @@ +package homewizard + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test ApiResponse +func TestUnmarshalApiResponse(t *testing.T) { + { + var res ApiResponse + + jsonstr := `{"product_type": "HWE-SKT","product_name": "P1 Meter","serial": "3c39e7aabbcc","firmware_version": "2.11","api_version": "v1"}` + assert.NoError(t, json.Unmarshal([]byte(jsonstr), &res)) + + assert.Equal(t, "HWE-SKT", res.ProductType) + assert.Equal(t, "v1", res.ApiVersion) + } +} + +// Test StateResponse +func TestUnmarshalStateResponse(t *testing.T) { + { + var res StateResponse + + jsonstr := `{"power_on": true,"switch_lock": false,"brightness": 255}` + assert.NoError(t, json.Unmarshal([]byte(jsonstr), &res)) + + assert.Equal(t, true, res.PowerOn) + } +} + +// Test DataResponse +func TestUnmarshalDataResponse(t *testing.T) { + { + var res DataResponse + + jsonstr := `{"wifi_ssid": "My Wi-Fi","wifi_strength": 100,"total_power_import_t1_kwh": 30.511,"total_power_export_t1_kwh": 85.951,"active_power_w": 543,"active_power_l1_w": 676}` + assert.NoError(t, json.Unmarshal([]byte(jsonstr), &res)) + + assert.Equal(t, float64(30.511), res.TotalPowerImportT1kWh+res.TotalPowerImportT2kWh+res.TotalPowerImportT3kWh+res.TotalPowerImportT4kWh) + assert.Equal(t, float64(543), res.ActivePowerW) + } +} diff --git a/templates/definition/charger/homewizard.yaml b/templates/definition/charger/homewizard.yaml new file mode 100644 index 000000000..fd839424a --- /dev/null +++ b/templates/definition/charger/homewizard.yaml @@ -0,0 +1,10 @@ +template: homewizard +products: + - brand: HomeWizard +group: switchsockets +params: + - name: host + - preset: switchsocket +render: | + type: homewizard + uri: http://{{ .host }} diff --git a/templates/definition/charger/tasmota.yaml b/templates/definition/charger/tasmota.yaml index fe3e2d46e..2ef4c5223 100644 --- a/templates/definition/charger/tasmota.yaml +++ b/templates/definition/charger/tasmota.yaml @@ -3,7 +3,6 @@ products: - brand: Tasmota group: switchsockets params: - - name: host - name: host - name: user required: false diff --git a/templates/definition/meter/homewizard.yaml b/templates/definition/meter/homewizard.yaml index 8d33094cc..6a2921604 100644 --- a/templates/definition/meter/homewizard.yaml +++ b/templates/definition/meter/homewizard.yaml @@ -5,22 +5,9 @@ products: generic: Wi-Fi P1 Meter params: - name: usage - choice: ["grid"] + choice: ["grid", "pv"] - name: host required: true render: | - type: custom - power: - source: http - uri: http://{{ .host }}/api/v1/data - method: GET - headers: - - content-type: application/json - jq: .active_power_w - energy: - source: http - uri: http://{{ .host }}/api/v1/data - method: GET - headers: - - content-type: application/json - jq: '[to_entries[] | select(.key|startswith("total_power_import_t")) | .value] | add' + type: homewizard + uri: http://{{ .host }} diff --git a/templates/docs/charger/homewizard_0.yaml b/templates/docs/charger/homewizard_0.yaml new file mode 100644 index 000000000..5b15f3600 --- /dev/null +++ b/templates/docs/charger/homewizard_0.yaml @@ -0,0 +1,16 @@ +product: + brand: HomeWizard + group: Schaltbare Steckdosen +render: + - default: | + type: template + template: homewizard + host: 192.0.2.2 # IP-Adresse oder Hostname + standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet (Optional) + advanced: | + type: template + template: homewizard + host: 192.0.2.2 # IP-Adresse oder Hostname + standbypower: 15 # Leistung oberhalb des angegebenen Wertes wird als Ladeleistung gewertet (Optional) + integrateddevice: # Optional + icon: # Icon in der Benutzeroberfläche (Optional) diff --git a/templates/docs/meter/homewizard_0.yaml b/templates/docs/meter/homewizard_0.yaml index 4ccb23f89..fa0f12cab 100644 --- a/templates/docs/meter/homewizard_0.yaml +++ b/templates/docs/meter/homewizard_0.yaml @@ -8,3 +8,9 @@ render: template: homewizard usage: grid host: 192.0.2.2 # IP-Adresse oder Hostname + - usage: pv + default: | + type: template + template: homewizard + usage: pv + host: 192.0.2.2 # IP-Adresse oder Hostname diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json index d6c17f100..9fddff3ae 100644 --- a/templates/evcc.io/brands.json +++ b/templates/evcc.io/brands.json @@ -67,6 +67,7 @@ "SmartPlugs": [ "AVM", "Homematic / Homematic IP", + "HomeWizard", "myStrom", "Shelly", "Tasmota", @@ -146,6 +147,7 @@ "GoodWe", "Growatt", "Homematic IP", + "HomeWizard", "Hoymiles", "Huawei", "IGEN Tech",