Skip to content

Commit 273c7ec

Browse files
committed
Support operation on nodes w/o phase relay
PV control works but number of phases can't be changed. Number of phases as reported by wallbox are used. - new PhaseMode.DISABLED Prevents any attempts to switch phase relay. Phase control buttons are disabled in UI. - new config: enable_phase_switching_on_host_only If set, node name passed as parameter to pvcontrol must match this config value to enable phase control.
1 parent 252054e commit 273c7ec

File tree

7 files changed

+48
-20
lines changed

7 files changed

+48
-20
lines changed

pvcontrol/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
parser.add_argument("-w", "--wallbox", default="SimulatedWallbox")
2525
parser.add_argument("-a", "--car", default="SimulatedCar")
2626
parser.add_argument("-c", "--config", default="{}")
27+
parser.add_argument("--hostname", default="", help="server hostname, can be used to enable/disable phase relay on k8s")
2728
parser.add_argument("--host", default="0.0.0.0", help="server host (default: 0.0.0.0)")
2829
parser.add_argument("--port", type=int, default=8080, help="server port (default: 8080)")
2930
parser.add_argument("--basehref", help="URL prefix to match ng base-href param (no leading /)")
@@ -42,6 +43,7 @@
4243
logger.info(f"Wallbox: {args.wallbox}")
4344
logger.info(f"Car: {args.car}")
4445
logger.info(f"config: {args.config}")
46+
logger.info(f"hostname:{args.hostname}")
4547
config = json.loads(args.config)
4648
for c in ["wallbox", "meter", "car", "controller"]:
4749
if c not in config:
@@ -50,7 +52,7 @@
5052
wallbox = WallboxFactory.newWallbox(args.wallbox, **config["wallbox"])
5153
meter = MeterFactory.newMeter(args.meter, wallbox, **config["meter"])
5254
car = CarFactory.newCar(args.car, **config["car"])
53-
controller = ChargeControllerFactory.newController(meter, wallbox, **config["controller"])
55+
controller = ChargeControllerFactory.newController(meter, wallbox, args.hostname, **config["controller"])
5456

5557
controller_scheduler = Scheduler(controller.get_config().cycle_time, controller.run)
5658
controller_scheduler.start()

pvcontrol/chargecontroller.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ChargeMode(str, enum.Enum):
3737

3838
@enum.unique
3939
class PhaseMode(str, enum.Enum):
40+
DISABLED = "DISABLED" # phase relay is not in operation
4041
AUTO = "AUTO" # PV switches between 1 and 3 phases
4142
CHARGE_1P = "CHARGE_1P"
4243
CHARGE_3P = "CHARGE_3P"
@@ -53,6 +54,7 @@ class ChargeControllerData(BaseData):
5354
class ChargeControllerConfig(BaseConfig):
5455
cycle_time: int = 30 # [s] control loop cycle time, used by scheduler
5556
enable_phase_switching: bool = True # set to False of phase relay is not in operation
57+
enable_phase_switching_on_host_only = "" # if set, phase switching is only allowed when running on specified host (env var: HOSTNAME)
5658
enable_auto_phase_switching: bool = True # automatic phase switching depending on available PV
5759
enable_charging_when_connecting_car: ChargeMode = ChargeMode.OFF
5860
line_voltage: float = 230 # [V]
@@ -80,7 +82,8 @@ class ChargeController(BaseService[ChargeControllerConfig, ChargeControllerData]
8082
"pvcontrol_controller_charged_energy_wh_total", "Energy charged into car by source", ["source"]
8183
)
8284

83-
def __init__(self, config: ChargeControllerConfig, meter: Meter, wallbox: Wallbox):
85+
# hostname - optional parameter to enable/disable phase switching depending on where pvcontrol runs (k8s hostname)
86+
def __init__(self, config: ChargeControllerConfig, meter: Meter, wallbox: Wallbox, hostname=""):
8487
super().__init__(config)
8588
self._meter = meter
8689
self._wallbox = wallbox
@@ -93,6 +96,9 @@ def __init__(self, config: ChargeControllerConfig, meter: Meter, wallbox: Wallbo
9396
self._last_energy_consumption = 0.0 # total counter value, must be initialized first with data from meter
9497
self._last_energy_consumption_grid = 0.0 # total counter value, must be initialized first with data from meter
9598
# config
99+
self._enable_phase_switching = self._is_enable_phase_switching(hostname)
100+
if not self._enable_phase_switching:
101+
self.set_phase_mode(PhaseMode.DISABLED)
96102
self._min_supported_current = wallbox.get_config().min_supported_current
97103
self._max_supported_current = wallbox.get_config().max_supported_current
98104
min_power_1phase = self._min_supported_current * config.line_voltage
@@ -110,11 +116,19 @@ def __init__(self, config: ChargeControllerConfig, meter: Meter, wallbox: Wallbo
110116
ChargeController._metrics_pvc_controller_charged_energy.labels("grid")
111117
ChargeController._metrics_pvc_controller_charged_energy.labels("pv")
112118

119+
def _is_enable_phase_switching(self, hostname: str) -> bool:
120+
if self.get_config().enable_phase_switching:
121+
require_hostname = self.get_config().enable_phase_switching_on_host_only
122+
return not require_hostname or require_hostname == hostname
123+
return False
124+
113125
def set_desired_mode(self, mode: ChargeMode) -> None:
114126
logger.info(f"set_desired_mode: {self.get_data().desired_mode} -> {mode}")
115127
self.get_data().desired_mode = mode
116128

117129
def set_phase_mode(self, mode: PhaseMode) -> None:
130+
if not self._enable_phase_switching:
131+
mode = PhaseMode.DISABLED
118132
self.get_data().phase_mode = mode
119133

120134
@_metrics_pvc_controller_processing.time()
@@ -212,8 +226,7 @@ def _converge_phases(self, m: MeterData, wb: WallboxData) -> bool:
212226
self._wallbox.trigger_reset()
213227
return True
214228

215-
config = self.get_config()
216-
if config.enable_phase_switching:
229+
if self._enable_phase_switching:
217230
available_power = -m.power_grid + wb.power
218231
desired_phases = self._desired_phases(available_power, wb.phases_in)
219232
if wb.error == 0 and desired_phases != wb.phases_in:
@@ -224,11 +237,7 @@ def _converge_phases(self, m: MeterData, wb: WallboxData) -> bool:
224237
# charging off and wait one cylce
225238
self._set_allow_charging(False, skip_delay=True)
226239
return True
227-
else:
228-
return False
229-
else:
230-
self.set_phase_mode(PhaseMode.CHARGE_1P if wb.phases_in == 1 else PhaseMode.CHARGE_3P)
231-
return False
240+
return False
232241

233242
def _desired_phases(self, available_power: float, current_phases: int):
234243
# TODO 2 phase charging
@@ -341,5 +350,5 @@ def _set_allow_charging(self, v: bool, skip_delay: bool = False):
341350

342351
class ChargeControllerFactory:
343352
@classmethod
344-
def newController(cls, meter: Meter, wb: Wallbox, **kwargs) -> ChargeController:
345-
return ChargeController(ChargeControllerConfig(**kwargs), meter, wb)
353+
def newController(cls, meter: Meter, wb: Wallbox, hostname="", **kwargs) -> ChargeController:
354+
return ChargeController(ChargeControllerConfig(**kwargs), meter, wb, hostname)

tests/test_chargecontroller.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,25 +274,25 @@ def test_3P(self):
274274

275275
c = self.controller.get_data()
276276
self.assertEqual(ChargeMode.OFF, c.mode)
277-
self.assertEqual(PhaseMode.CHARGE_3P, c.phase_mode)
277+
self.assertEqual(PhaseMode.DISABLED, c.phase_mode)
278278
self.assertEqual(3, self.wallbox.get_data().phases_in)
279279

280280
self.controller.set_phase_mode(PhaseMode.CHARGE_1P)
281281
self.controller.run()
282-
self.assertEqual(PhaseMode.CHARGE_3P, c.phase_mode)
282+
self.assertEqual(PhaseMode.DISABLED, c.phase_mode)
283283
self.assertEqual(3, self.wallbox.get_data().phases_in)
284284

285285
def test_1P(self):
286286
self.controller.run() # init
287287
c = self.controller.get_data()
288288

289289
self.assertEqual(ChargeMode.OFF, c.mode)
290-
self.assertEqual(PhaseMode.CHARGE_1P, c.phase_mode)
290+
self.assertEqual(PhaseMode.DISABLED, c.phase_mode)
291291
self.assertEqual(1, self.wallbox.get_data().phases_in)
292292

293293
self.controller.set_phase_mode(PhaseMode.AUTO)
294294
self.controller.run()
295-
self.assertEqual(PhaseMode.CHARGE_1P, c.phase_mode)
295+
self.assertEqual(PhaseMode.DISABLED, c.phase_mode)
296296
self.assertEqual(1, self.wallbox.get_data().phases_in)
297297

298298

ui/src/app/app.component.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ mat-card-content {
2626
// = mat-primary + mat-body-strong
2727
.active {
2828
font-weight: 500;
29-
color: mat.get-color-from-palette(themes.$light-primary, 'default');
29+
color: mat.get-color-from-palette(themes.$light-primary, 'default') !important;
3030
}
3131
:host-context(.dark-theme) {
3232
.active {
3333
font-weight: 500;
34-
color: mat.get-color-from-palette(themes.$dark-primary, 'default');
34+
color: mat.get-color-from-palette(themes.$dark-primary, 'default') !important;
3535
}
3636
}
3737

ui/src/app/app.component.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ describe('AppComponent', () => {
161161

162162
expect(component.pvControl).toEqual(pvControlData);
163163
expect(component.chargeModeControl.value).toBe(ChargeMode.PV_ONLY);
164+
expect(component.phaseModeControl.enabled).toBeTrue();
164165
expect(component.phaseModeControl.value).toBe(PhaseMode.CHARGE_1P);
165166
expect(await chargeModePvOnly.isChecked()).toBeTrue();
166167
expect(await phaseModeCharge1P.isChecked()).toBeTrue();
@@ -248,6 +249,16 @@ describe('AppComponent', () => {
248249
expect(await phaseModeCharge1P.isChecked()).toBeTrue();
249250
expect(await phaseModeAuto.isChecked()).toBeFalse();
250251
});
252+
253+
it('should support disabled phase relay', async () => {
254+
pvControlData.controller.phase_mode = PhaseMode.DISABLED;
255+
httpMock.expectOne('./api/pvcontrol').flush(pvControlData);
256+
fixture.detectChanges();
257+
258+
expect(component.phaseModeControl.disabled).toBeTrue();
259+
expect(await phaseModeAuto.isChecked()).toBeFalse();
260+
expect(await phaseModeAuto.isDisabled()).toBeTrue();
261+
});
251262
});
252263

253264
describe('AppComponent', () => {

ui/src/app/app.component.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class AppComponent implements OnInit, OnDestroy {
5858
error: 0,
5959
allow_charging: false,
6060
max_current: 0,
61-
phases_in: 3,
61+
phases_in: 1,
6262
phases_out: 0,
6363
power: 0,
6464
temperature: 0,
@@ -67,7 +67,7 @@ export class AppComponent implements OnInit, OnDestroy {
6767
error: 0,
6868
mode: ChargeMode.OFF,
6969
desired_mode: ChargeMode.OFF,
70-
phase_mode: PhaseMode.AUTO,
70+
phase_mode: PhaseMode.DISABLED,
7171
},
7272
car: {
7373
error: 0,
@@ -89,7 +89,7 @@ export class AppComponent implements OnInit, OnDestroy {
8989
chargingStateIcon = 'power_off';
9090

9191
chargeModeControl = this.fb.control(ChargeMode.OFF);
92-
phaseModeControl = this.fb.control(PhaseMode.AUTO);
92+
phaseModeControl = this.fb.control({value: PhaseMode.DISABLED, disabled: true});
9393

9494
constructor(
9595
private appRef: ApplicationRef, private fb: FormBuilder, private snackBar: MatSnackBar,
@@ -176,6 +176,11 @@ export class AppComponent implements OnInit, OnDestroy {
176176
}
177177
this.chargeModeControl.setValue(mode);
178178
this.phaseModeControl.setValue(pv.controller.phase_mode);
179+
if (pv.controller.phase_mode === PhaseMode.DISABLED) {
180+
this.phaseModeControl.disable()
181+
} else {
182+
this.phaseModeControl.enable()
183+
}
179184
},
180185
error: () => { }
181186
});

ui/src/app/pv-control.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export enum ChargeMode {
3131
}
3232

3333
export enum PhaseMode {
34+
DISABLED = 'DISABLED',
3435
AUTO = 'AUTO',
3536
CHARGE_1P = 'CHARGE_1P',
3637
CHARGE_3P = 'CHARGE_3P',

0 commit comments

Comments
 (0)