Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion charger/homewizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func NewHomeWizardFromConfig(other map[string]any) (api.Charger, error) {

// NewHomeWizard creates HomeWizard charger
func NewHomeWizard(embed embed, uri string, usage string, standbypower float64, cache time.Duration) (*HomeWizard, error) {
conn, err := homewizard.NewConnection(uri, usage, cache)
conn, err := homewizard.NewConnection(uri, usage, 1, cache)
if err != nil {
return nil, err
}
Expand Down
8 changes: 5 additions & 3 deletions meter/homewizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,23 @@ func NewHomeWizardFromConfig(other map[string]any) (api.Meter, error) {
cc := struct {
URI string
Usage string
Phase int
Cache time.Duration
}{
Phase: 1,
Cache: time.Second,
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

return NewHomeWizard(cc.URI, cc.Usage, cc.Cache)
return NewHomeWizard(cc.URI, cc.Usage, cc.Phase, cc.Cache)
}

// NewHomeWizard creates HomeWizard meter
func NewHomeWizard(uri string, usage string, cache time.Duration) (*HomeWizard, error) {
conn, err := homewizard.NewConnection(uri, usage, cache)
func NewHomeWizard(uri string, usage string, phase int, cache time.Duration) (*HomeWizard, error) {
conn, err := homewizard.NewConnection(uri, usage, phase, cache)
if err != nil {
return nil, err
}
Expand Down
55 changes: 49 additions & 6 deletions meter/homewizard/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,28 @@ type Connection struct {
*request.Helper
uri string
usage string
phase int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entire single phase mapping is a bad idea, please remove.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why is it a bad idea? Load limits apply to each phase and so the measurements should be counted to the right phase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why is it a bad idea? Load limits apply to each phase and so the measurements should be counted to the right phase.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We‘re not doing this for any device

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove or open fresh pr without

ProductType string
dataG util.Cacheable[DataResponse]
stateG util.Cacheable[StateResponse]
}

// NewConnection creates a homewizard connection
func NewConnection(uri string, usage string, cache time.Duration) (*Connection, error) {
func NewConnection(uri string, usage string, phase int, cache time.Duration) (*Connection, error) {
if uri == "" {
return nil, errors.New("missing uri")
}

if phase < 1 || phase > 3 {
return nil, errors.New("phase must be between 1 and 3")
}

log := util.NewLogger("homewizard")
c := &Connection{
Helper: request.NewHelper(log),
uri: fmt.Sprintf("%s/api", util.DefaultScheme(strings.TrimRight(uri, "/"), "http")),
usage: usage,
phase: phase,
}

c.Client.Transport = request.NewTripper(log, transport.Insecure())
Expand Down Expand Up @@ -101,7 +107,7 @@ func (c *Connection) Enabled() (bool, error) {
// CurrentPower implements the api.Meter interface
func (c *Connection) CurrentPower() (float64, error) {
res, err := c.dataG.Get()
if c.usage == "pv" {
if c.usage == "pv" || c.usage == "battery" {
return -res.ActivePowerW, err
}
return res.ActivePowerW, err
Expand All @@ -110,16 +116,36 @@ func (c *Connection) CurrentPower() (float64, error) {
// TotalEnergy implements the api.MeterEnergy interface
func (c *Connection) TotalEnergy() (float64, error) {
res, err := c.dataG.Get()
if c.usage == "pv" {
return res.TotalPowerExportT1kWh + res.TotalPowerExportT2kWh + res.TotalPowerExportT3kWh + res.TotalPowerExportT4kWh, err
if c.usage == "pv" || c.usage == "battery" {
return res.TotalPowerExportkWh, err
}
return res.TotalPowerImportT1kWh + res.TotalPowerImportT2kWh + res.TotalPowerImportT3kWh + res.TotalPowerImportT4kWh, err
return res.TotalPowerImportkWh, err
}

// Currents implements the api.PhaseCurrents interface
func (c *Connection) Currents() (float64, float64, float64, error) {
res, err := c.dataG.Get()
if c.usage == "pv" {

// Single-phase meters only have one current reading
if c.ProductType == "HWE-KWH1" || c.ProductType == "SDM230-wifi" {
current := res.ActiveCurrentA
if c.usage == "pv" || c.usage == "battery" {
current = -current
}

// Return current on configured phase
switch c.phase {
case 1:
return current, 0, 0, err
case 2:
return 0, current, 0, err
case 3:
return 0, 0, current, err
}
}

// Three-phase meters have separate current readings per phase
if c.usage == "pv" || c.usage == "battery" {
return -res.ActiveCurrentL1A, -res.ActiveCurrentL2A, -res.ActiveCurrentL3A, err
}
return res.ActiveCurrentL1A, res.ActiveCurrentL2A, res.ActiveCurrentL3A, err
Expand All @@ -128,5 +154,22 @@ func (c *Connection) Currents() (float64, float64, float64, error) {
// Voltages implements the api.PhaseVoltages interface
func (c *Connection) Voltages() (float64, float64, float64, error) {
res, err := c.dataG.Get()

// Single-phase meters only have one voltage reading
if c.ProductType == "HWE-KWH1" || c.ProductType == "SDM230-wifi" {
voltage := res.ActiveVoltageV

// Return voltage on configured phase
switch c.phase {
case 1:
return voltage, 0, 0, err
case 2:
return 0, voltage, 0, err
case 3:
return 0, 0, voltage, err
}
}

// Three-phase meters have separate voltage readings per phase
return res.ActiveVoltageL1V, res.ActiveVoltageL2V, res.ActiveVoltageL3V, err
}
26 changes: 11 additions & 15 deletions meter/homewizard/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,15 @@ type StateResponse struct {
// 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"`
TotalPowerExportT1kWh float64 `json:"total_power_export_t1_kwh"`
TotalPowerExportT2kWh float64 `json:"total_power_export_t2_kwh"`
TotalPowerExportT3kWh float64 `json:"total_power_export_t3_kwh"`
TotalPowerExportT4kWh float64 `json:"total_power_export_t4_kwh"`
ActiveCurrentL1A float64 `json:"active_current_l1_a"`
ActiveCurrentL2A float64 `json:"active_current_l2_a"`
ActiveCurrentL3A float64 `json:"active_current_l3_a"`
ActiveVoltageL1V float64 `json:"active_voltage_l1_v"`
ActiveVoltageL2V float64 `json:"active_voltage_l2_v"`
ActiveVoltageL3V float64 `json:"active_voltage_l3_v"`
ActivePowerW float64 `json:"active_power_w"`
TotalPowerImportkWh float64 `json:"total_power_import_kwh"`
TotalPowerExportkWh float64 `json:"total_power_export_kwh"`
ActiveCurrentA float64 `json:"active_current_a"`
ActiveCurrentL1A float64 `json:"active_current_l1_a"`
ActiveCurrentL2A float64 `json:"active_current_l2_a"`
ActiveCurrentL3A float64 `json:"active_current_l3_a"`
ActiveVoltageV float64 `json:"active_voltage_v"`
ActiveVoltageL1V float64 `json:"active_voltage_l1_v"`
ActiveVoltageL2V float64 `json:"active_voltage_l2_v"`
ActiveVoltageL3V float64 `json:"active_voltage_l3_v"`
Comment on lines +23 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): Switching from tariff-based totals to aggregate import/export fields may affect compatibility with existing devices/firmware.

This change assumes total_power_import_kwh and total_power_export_kwh exist on all supported HomeWizard devices/firmware. Please verify that older KWH/P1 versions don’t expose only TotalPowerImportT{1..4}kWh / TotalPowerExportT{1..4}kWh. If some do, consider preferring the aggregate fields when available and falling back to summing the tariff fields when aggregates are missing or zero.

}
10 changes: 5 additions & 5 deletions meter/homewizard/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ func TestUnmarshalKwhDataResponse(t *testing.T) {
{
var res DataResponse
// https://www.homewizard.com/shop/wi-fi-kwh-meter-1-phase/
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": 28,"active_power_l2_w": 0,"active_power_l3_w": -181,"active_voltage_l1_v": 235.4,"active_voltage_l2_v": 235.8,"active_voltage_l3_v": 236.1,"active_current_l1_a": 1.19,"active_current_l2_a": 0.37,"active_current_l3_a": -0.93}`
jsonstr := `{"wifi_ssid": "My Wi-Fi","wifi_strength": 100,"total_power_import_kwh": 30.511,"total_power_export_kwh": 85.951,"total_power_import_t1_kwh": 30.511,"total_power_export_t1_kwh": 85.951,"active_power_w": 543,"active_power_l1_w": 28,"active_power_l2_w": 0,"active_power_l3_w": -181,"active_voltage_l1_v": 235.4,"active_voltage_l2_v": 235.8,"active_voltage_l3_v": 236.1,"active_current_l1_a": 1.19,"active_current_l2_a": 0.37,"active_current_l3_a": -0.93}`
require.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(85.951), res.TotalPowerExportT1kWh+res.TotalPowerExportT2kWh+res.TotalPowerExportT3kWh+res.TotalPowerExportT4kWh)
assert.Equal(t, float64(30.511), res.TotalPowerImportkWh)
assert.Equal(t, float64(85.951), res.TotalPowerExportkWh)
Comment on lines +44 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Assert new current/voltage aggregate fields in kWh data response

Please also extend this test to assert the ActiveCurrentA and ActiveVoltageV values when present in the JSON, so changes to their JSON tags or types are caught by tests.

Suggested implementation:

		jsonstr := `{"wifi_ssid":"redacted","wifi_strength":78,"smr_version":50,"meter_model":"Landis + Gyr","unique_id":"redacted","active_tariff":2,"total_power_import_kwh":18664.997,"total_power_import_t1_kwh":10909.724,"total_power_import_t2_kwh":7755.273,"total_power_export_kwh":13823.608,"total_power_export_t1_kwh":4243.981,"total_power_export_t2_kwh":9579.627,"active_power_w":203.000,"active_power_l1_w":-21.000,"active_power_l2_w":57.000,"active_power_l3_w":168.000,"active_voltage_v":226.000,"active_voltage_l1_v":228.000,"active_voltage_l2_v":226.000,"active_voltage_l3_v":225.000,"active_current_a":1.091,"active_current_l1_a":-0.092,"active_current_l2_a":0.252,"active_current_l3_a":0.747,"voltage_sag_l1_count":12.000,"voltage_sag_l2_count":12.000,"voltage_sag_l3_count":19.000,"voltage_swell_l1_count":5055.000,"voltage_swell_l2_count":1950.000,"voltage_swell_l3_count":0.000,"any_power_fail_count":12.000,"long_power_fail_count":2.000,"total_gas_m3":5175.363,"gas_timestamp":241106093006,"gas_unique_id":"redacted","external":[{"unique_id":"redacted","type":"gas_meter","timestamp":241106093006,"value":5175.363,"unit":"m3"}]}`
		assert.Equal(t, float64(18664.997), res.TotalPowerImportkWh)
		assert.Equal(t, float64(13823.608), res.TotalPowerExportkWh)
		assert.Equal(t, float64(203), res.ActivePowerW)
		assert.Equal(t, float64(226), res.ActiveVoltageV)
		assert.Equal(t, float64(1.091), res.ActiveCurrentA)
		assert.Equal(t, float64(228), res.ActiveVoltageL1V)

assert.Equal(t, float64(543), res.ActivePowerW)
assert.Equal(t, float64(235.4), res.ActiveVoltageL1V)
assert.Equal(t, float64(235.8), res.ActiveVoltageL2V)
Expand All @@ -61,8 +61,8 @@ func TestUnmarshalP1DataResponse(t *testing.T) {
jsonstr := `{"wifi_ssid":"redacted","wifi_strength":78,"smr_version":50,"meter_model":"Landis + Gyr","unique_id":"redacted","active_tariff":2,"total_power_import_kwh":18664.997,"total_power_import_t1_kwh":10909.724,"total_power_import_t2_kwh":7755.273,"total_power_export_kwh":13823.608,"total_power_export_t1_kwh":4243.981,"total_power_export_t2_kwh":9579.627,"active_power_w":203.000,"active_power_l1_w":-21.000,"active_power_l2_w":57.000,"active_power_l3_w":168.000,"active_voltage_l1_v":228.000,"active_voltage_l2_v":226.000,"active_voltage_l3_v":225.000,"active_current_a":1.091,"active_current_l1_a":-0.092,"active_current_l2_a":0.252,"active_current_l3_a":0.747,"voltage_sag_l1_count":12.000,"voltage_sag_l2_count":12.000,"voltage_sag_l3_count":19.000,"voltage_swell_l1_count":5055.000,"voltage_swell_l2_count":1950.000,"voltage_swell_l3_count":0.000,"any_power_fail_count":12.000,"long_power_fail_count":2.000,"total_gas_m3":5175.363,"gas_timestamp":241106093006,"gas_unique_id":"redacted","external":[{"unique_id":"redacted","type":"gas_meter","timestamp":241106093006,"value":5175.363,"unit":"m3"}]}`
require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))

assert.Equal(t, float64(18664.997), res.TotalPowerImportT1kWh+res.TotalPowerImportT2kWh+res.TotalPowerImportT3kWh+res.TotalPowerImportT4kWh)
assert.Equal(t, float64(13823.608), res.TotalPowerExportT1kWh+res.TotalPowerExportT2kWh+res.TotalPowerExportT3kWh+res.TotalPowerExportT4kWh)
assert.Equal(t, float64(18664.997), res.TotalPowerImportkWh)
assert.Equal(t, float64(13823.608), res.TotalPowerExportkWh)
assert.Equal(t, float64(203), res.ActivePowerW)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for NewConnection phase validation and default phase behavior

Since NewConnection now validates that phase is in [1,3], add tests that:

  • Call NewConnection with invalid phases (e.g. 0, 4, and a negative) and assert it returns an error mentioning the valid range.
  • Use the default Phase: 1 in charger and meter configs, ensure the resulting Connection is valid, and confirm the internal phase is 1.

This will guard against regressions in phase validation and defaulting during future refactors.

assert.Equal(t, float64(228), res.ActiveVoltageL1V)
assert.Equal(t, float64(226), res.ActiveVoltageL2V)
Expand Down
3 changes: 3 additions & 0 deletions templates/definition/charger/homewizard.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
template: homewizard
products:
- brand: HomeWizard
description:
en: Smart Socket (HWE-SKT)
de: Smart-Steckdose (HWE-SKT)
group: switchsockets
params:
- name: host
Expand Down
15 changes: 14 additions & 1 deletion templates/definition/meter/homewizard-kwh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,24 @@ products:
- brand: HomeWizard
description:
generic: kWh Meter
en: kWh Meter (HWE-KWH1, HWE-KWH3, SDM230-wifi, SDM630-wifi)
de: kWh-Zähler (HWE-KWH1, HWE-KWH3, SDM230-wifi, SDM630-wifi)
params:
- name: usage
choice: ["pv", "charge"]
choice: ["pv", "charge", "battery"]
- name: host
- name: phase
default: 1
choice: [1, 2, 3]
advanced: true
description:
en: Installation phase
de: Installationsphase
help:
en: Phase on which the single-phase meter is installed (L1=1, L2=2, L3=3). Only relevant for HWE-KWH1 and SDM230-wifi.
de: Phase an der der einphasige Zähler installiert ist (L1=1, L2=2, L3=3). Nur relevant für HWE-KWH1 und SDM230-wifi.
render: |
type: homewizard
uri: http://{{ .host }}
usage: {{ .usage }}
phase: {{ .phase }}
Loading