Skip to content

Commit a2feb45

Browse files
committed
Add support for both 1-phase as 3-phase meters
1 parent 840133c commit a2feb45

File tree

8 files changed

+162
-118
lines changed

8 files changed

+162
-118
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ require (
7575
github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543
7676
github.com/mabunixda/wattpilot v1.8.5
7777
github.com/mitchellh/go-homedir v1.1.0
78-
github.com/mluiten/evcc-homewizard-v2 v0.2.0
78+
github.com/mluiten/evcc-homewizard-v2 v0.3.0
7979
github.com/modelcontextprotocol/go-sdk v1.1.0
8080
github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e
8181
github.com/mxschmitt/golang-combinations v1.2.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,8 +516,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
516516
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
517517
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
518518
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
519-
github.com/mluiten/evcc-homewizard-v2 v0.2.0 h1:W9bhbxQN5PCzaIxkFF68VJk5gA6GOmMgEqmBjh2gGks=
520-
github.com/mluiten/evcc-homewizard-v2 v0.2.0/go.mod h1:0gWZsWfzmBhqoG0SEbHdx6ohOE7EyRl+2ZQHos+y/yM=
519+
github.com/mluiten/evcc-homewizard-v2 v0.3.0 h1:b4ZzhXnBE/ZpeiDsMXhfSpKZmNz+kb5jmTgSgA5gJVQ=
520+
github.com/mluiten/evcc-homewizard-v2 v0.3.0/go.mod h1:0gWZsWfzmBhqoG0SEbHdx6ohOE7EyRl+2ZQHos+y/yM=
521521
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
522522
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
523523
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

meter/homewizard-battery.go

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func init() {
1919
type HomeWizardBattery struct {
2020
log *util.Logger
2121
device *device.BatteryDevice
22-
controller *device.P1Device
22+
controller *device.P1MeterDevice
2323
controllerOnce sync.Once
2424
capacity float64
2525
maxCharge float64 // Maximum charge power in W
@@ -68,7 +68,7 @@ func NewHomeWizardBatteryFromConfig(other map[string]any) (api.Meter, error) {
6868
}
6969

7070
// getController resolves the controller P1 meter (lazy initialization to avoid timing issues)
71-
func (m *HomeWizardBattery) getController() (*device.P1Device, error) {
71+
func (m *HomeWizardBattery) getController() (*device.P1MeterDevice, error) {
7272
var err error
7373

7474
m.controllerOnce.Do(func() {
@@ -88,7 +88,8 @@ func (m *HomeWizardBattery) getController() (*device.P1Device, error) {
8888
return
8989
}
9090

91-
m.controller = controllerP1.device
91+
// Store P1MeterDevice for battery control
92+
m.controller = controllerP1.p1MeterDevice
9293
m.log.DEBUG.Printf("resolved controller: %s", m.controller.Host())
9394
})
9495

@@ -117,35 +118,21 @@ var _ api.Meter = (*HomeWizardBattery)(nil)
117118

118119
// CurrentPower implements the api.Meter interface
119120
func (m *HomeWizardBattery) CurrentPower() (float64, error) {
120-
// Get power directly from battery device
121-
measurement, err := m.device.GetMeasurement()
122-
if err != nil {
123-
return 0, err
124-
}
125-
// Invert the battery power, because HW reports negative = discharging and positive = charging
126-
return -1 * measurement.PowerW, nil
121+
return m.device.GetPower()
127122
}
128123

129124
var _ api.MeterEnergy = (*HomeWizardBattery)(nil)
130125

131126
// TotalEnergy implements the api.MeterEnergy interface
132127
func (m *HomeWizardBattery) TotalEnergy() (float64, error) {
133-
measurement, err := m.device.GetMeasurement()
134-
if err != nil {
135-
return 0, err
136-
}
137-
return measurement.EnergyImportkWh, nil
128+
return m.device.GetTotalEnergy()
138129
}
139130

140131
var _ api.Battery = (*HomeWizardBattery)(nil)
141132

142133
// Soc implements the api.Battery interface
143134
func (m *HomeWizardBattery) Soc() (float64, error) {
144-
measurement, err := m.device.GetMeasurement()
145-
if err != nil {
146-
return 0, err
147-
}
148-
return measurement.StateOfChargePct, nil
135+
return m.device.GetSoc()
149136
}
150137

151138
var _ api.BatteryCapacity = (*HomeWizardBattery)(nil)
@@ -189,7 +176,7 @@ func (m *HomeWizardBattery) SetBatteryMode(mode api.BatteryMode) error {
189176

190177
m.log.INFO.Printf("converted to HomeWizard mode: %s", hwMode)
191178

192-
// Set battery mode via controller P1 meter
179+
// Set battery mode via controller P1 meter's wrapper method
193180
if err := controller.SetBatteryMode(hwMode); err != nil {
194181
m.log.ERROR.Printf("failed to set battery mode: %v", err)
195182
return err

meter/homewizard-common.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package meter
2+
3+
import (
4+
"github.com/evcc-io/evcc/api"
5+
"github.com/evcc-io/evcc/util"
6+
)
7+
8+
// meterDevice interface abstracts P1MeterDevice and KWHMeterDevice
9+
type meterDevice interface {
10+
GetPower(invertForPV bool) (float64, error)
11+
GetPhasePowers(phases int, invertForPV bool) (float64, float64, float64, error)
12+
GetPhaseCurrents(phases int) (float64, float64, float64, error)
13+
GetPhaseVoltages(phases int) (float64, float64, float64, error)
14+
GetTotalEnergy(usePVExport bool) (float64, error)
15+
}
16+
17+
// HomeWizardMeter provides common functionality for all HomeWizard meters
18+
type HomeWizardMeter struct {
19+
log *util.Logger
20+
device meterDevice
21+
usage string // "pv", "grid", etc.
22+
phases int // 1 or 3
23+
}
24+
25+
var _ api.Meter = (*HomeWizardMeter)(nil)
26+
27+
// CurrentPower implements the api.Meter interface
28+
func (m *HomeWizardMeter) CurrentPower() (float64, error) {
29+
return m.device.GetPower(m.usage == "pv")
30+
}
31+
32+
var _ api.MeterEnergy = (*HomeWizardMeter)(nil)
33+
34+
// TotalEnergy implements the api.MeterEnergy interface
35+
func (m *HomeWizardMeter) TotalEnergy() (float64, error) {
36+
// For PV meters, return export energy (production)
37+
// For grid meters, return import energy (consumption)
38+
return m.device.GetTotalEnergy(m.usage == "pv")
39+
}
40+
41+
var _ api.PhaseCurrents = (*HomeWizardMeter)(nil)
42+
43+
// Currents implements the api.PhaseCurrents interface
44+
func (m *HomeWizardMeter) Currents() (float64, float64, float64, error) {
45+
return m.device.GetPhaseCurrents(m.phases)
46+
}
47+
48+
var _ api.PhaseVoltages = (*HomeWizardMeter)(nil)
49+
50+
// Voltages implements the api.PhaseVoltages interface
51+
func (m *HomeWizardMeter) Voltages() (float64, float64, float64, error) {
52+
return m.device.GetPhaseVoltages(m.phases)
53+
}
54+
55+
var _ api.PhasePowers = (*HomeWizardMeter)(nil)
56+
57+
// Powers implements the api.PhasePowers interface
58+
func (m *HomeWizardMeter) Powers() (float64, float64, float64, error) {
59+
return m.device.GetPhasePowers(m.phases, m.usage == "pv")
60+
}
61+
62+
var _ api.PhaseGetter = (*HomeWizardMeter)(nil)
63+
64+
// GetPhases implements the api.PhaseGetter interface
65+
func (m *HomeWizardMeter) GetPhases() (int, error) {
66+
return m.phases, nil
67+
}

meter/homewizard-kwh.go

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@ func init() {
1313
registry.Add("homewizard-kwh", NewHomeWizardKWHFromConfig)
1414
}
1515

16-
// HomeWizardKWH implements the api.Meter interface for kWh meters
16+
// HomeWizardKWH is a wrapper for kWh meters using the common HomeWizardMeter base
1717
type HomeWizardKWH struct {
18-
log *util.Logger
19-
device *device.KWHDevice
20-
usage string // "pv" or "grid"
18+
*HomeWizardMeter
2119
}
2220

2321
func NewHomeWizardKWHFromConfig(other map[string]any) (api.Meter, error) {
2422
cc := struct {
2523
Host string
2624
Token string
27-
Usage string
25+
Usage string // "pv" or "grid"
26+
Phases int // 1 or 3
2827
Timeout time.Duration
2928
}{
3029
Usage: "pv",
30+
Phases: 3,
3131
Timeout: device.DefaultTimeout,
3232
}
3333

@@ -40,46 +40,29 @@ func NewHomeWizardKWHFromConfig(other map[string]any) (api.Meter, error) {
4040
return nil, fmt.Errorf("missing host or token - run 'evcc token homewizard'")
4141
}
4242

43-
m := &HomeWizardKWH{
44-
log: util.NewLogger("homewizard-kwh"),
45-
device: device.NewKWHDevice(cc.Host, cc.Token, cc.Timeout),
46-
usage: cc.Usage,
43+
// Validate phases
44+
if cc.Phases != 1 && cc.Phases != 3 {
45+
return nil, fmt.Errorf("invalid phases value %d: must be 1 or 3", cc.Phases)
4746
}
4847

48+
log := util.NewLogger("homewizard-kwh")
49+
kwhDevice := device.NewKWHMeterDevice(cc.Host, cc.Token, cc.Timeout)
50+
4951
// Start device connection and wait for it to succeed
50-
if err := m.device.StartAndWait(cc.Timeout); err != nil {
52+
if err := kwhDevice.StartAndWait(cc.Timeout); err != nil {
5153
return nil, err
5254
}
5355

54-
m.log.INFO.Printf("configured kWh meter at %s", cc.Host)
56+
log.INFO.Printf("configured kWh meter at %s (%d-phase, usage=%s)", cc.Host, cc.Phases, cc.Usage)
5557

56-
return m, nil
57-
}
58-
59-
var _ api.Meter = (*HomeWizardKWH)(nil)
60-
61-
// CurrentPower implements the api.Meter interface
62-
func (m *HomeWizardKWH) CurrentPower() (float64, error) {
63-
measurement, err := m.device.GetMeasurement()
64-
if err != nil {
65-
return 0, err
66-
}
67-
68-
// Invert power for PV (production shows as negative)
69-
// Don't invert for grid (import = positive, export = negative)
70-
if m.usage == "pv" {
71-
return -1 * measurement.PowerW, nil
58+
m := &HomeWizardKWH{
59+
HomeWizardMeter: &HomeWizardMeter{
60+
log: log,
61+
device: kwhDevice,
62+
usage: cc.Usage,
63+
phases: cc.Phases,
64+
},
7265
}
73-
return measurement.PowerW, nil
74-
}
7566

76-
var _ api.MeterEnergy = (*HomeWizardKWH)(nil)
77-
78-
// TotalEnergy implements the api.MeterEnergy interface
79-
func (m *HomeWizardKWH) TotalEnergy() (float64, error) {
80-
measurement, err := m.device.GetMeasurement()
81-
if err != nil {
82-
return 0, err
83-
}
84-
return measurement.EnergyImportkWh, nil
67+
return m, nil
8568
}

meter/homewizard-p1.go

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@ func init() {
1313
registry.Add("homewizard-p1", NewHomeWizardP1FromConfig)
1414
}
1515

16-
// HomeWizardP1 implements the api.Meter interface for P1 meters
16+
// HomeWizardP1 is a wrapper for P1 meters using the common HomeWizardMeter base
1717
type HomeWizardP1 struct {
18-
log *util.Logger
19-
device *device.P1Device
18+
*HomeWizardMeter
19+
p1MeterDevice *device.P1MeterDevice // Keep reference for battery control
2020
}
2121

2222
func NewHomeWizardP1FromConfig(other map[string]any) (api.Meter, error) {
2323
cc := struct {
2424
Host string
2525
Token string
26+
Phases int // 1 or 3
2627
Timeout time.Duration
2728
}{
29+
Phases: 3,
2830
Timeout: device.DefaultTimeout,
2931
}
3032

@@ -37,61 +39,37 @@ func NewHomeWizardP1FromConfig(other map[string]any) (api.Meter, error) {
3739
return nil, fmt.Errorf("missing host or token - run 'evcc token homewizard'")
3840
}
3941

40-
m := &HomeWizardP1{
41-
log: util.NewLogger("homewizard-p1"),
42-
device: device.NewP1Device(cc.Host, cc.Token, cc.Timeout),
42+
// Validate phases
43+
if cc.Phases != 1 && cc.Phases != 3 {
44+
return nil, fmt.Errorf("invalid phases value %d: must be 1 or 3", cc.Phases)
4345
}
4446

45-
// Start device connection and wait for it to succeed
46-
if err := m.device.StartAndWait(cc.Timeout); err != nil {
47-
return nil, err
48-
}
47+
log := util.NewLogger("homewizard-p1")
4948

50-
m.log.INFO.Printf("configured P1 meter at %s", cc.Host)
49+
// Create P1MeterDevice (includes battery control)
50+
p1MeterDevice := device.NewP1MeterDevice(cc.Host, cc.Token, cc.Timeout)
5151

52-
return m, nil
53-
}
54-
55-
var _ api.Meter = (*HomeWizardP1)(nil)
56-
57-
// CurrentPower implements the api.Meter interface
58-
func (m *HomeWizardP1) CurrentPower() (float64, error) {
59-
measurement, err := m.device.GetMeasurement()
60-
if err != nil {
61-
return 0, err
52+
// Start device connection and wait for it to succeed
53+
if err := p1MeterDevice.StartAndWait(cc.Timeout); err != nil {
54+
return nil, err
6255
}
63-
return measurement.PowerW, nil
64-
}
6556

66-
var _ api.MeterEnergy = (*HomeWizardP1)(nil)
57+
log.INFO.Printf("configured P1 meter at %s (%d-phase)", cc.Host, cc.Phases)
6758

68-
// TotalEnergy implements the api.MeterEnergy interface
69-
func (m *HomeWizardP1) TotalEnergy() (float64, error) {
70-
measurement, err := m.device.GetMeasurement()
71-
if err != nil {
72-
return 0, err
59+
m := &HomeWizardP1{
60+
HomeWizardMeter: &HomeWizardMeter{
61+
log: log,
62+
device: p1MeterDevice,
63+
usage: "grid", // P1 meters are always grid meters
64+
phases: cc.Phases,
65+
},
66+
p1MeterDevice: p1MeterDevice,
7367
}
74-
return measurement.EnergyImportT1kWh + measurement.EnergyImportT2kWh, nil
75-
}
76-
77-
var _ api.PhaseCurrents = (*HomeWizardP1)(nil)
7868

79-
// Currents implements the api.PhaseCurrents interface
80-
func (m *HomeWizardP1) Currents() (float64, float64, float64, error) {
81-
measurement, err := m.device.GetMeasurement()
82-
if err != nil {
83-
return 0, 0, 0, err
84-
}
85-
return measurement.CurrentL1A, measurement.CurrentL2A, measurement.CurrentL3A, nil
69+
return m, nil
8670
}
8771

88-
var _ api.PhaseVoltages = (*HomeWizardP1)(nil)
89-
90-
// Voltages implements the api.PhaseVoltages interface
91-
func (m *HomeWizardP1) Voltages() (float64, float64, float64, error) {
92-
measurement, err := m.device.GetMeasurement()
93-
if err != nil {
94-
return 0, 0, 0, err
95-
}
96-
return measurement.VoltageL1V, measurement.VoltageL2V, measurement.VoltageL3V, nil
72+
// SetBatteryMode sets battery mode via P1 meter (for battery controller)
73+
func (m *HomeWizardP1) SetBatteryMode(mode string) error {
74+
return m.p1MeterDevice.SetBatteryMode(mode)
9775
}

0 commit comments

Comments
 (0)