Skip to content

Commit c50eca0

Browse files
lackasCFenner
andauthored
feat(heat pump): add COP, compressor sensors, refrigerant circuit and heating rod stats (#689)
* feat(heat pump): add COP, compressor sensors, refrigerant circuit and heating rod stats Add comprehensive heat pump monitoring for Vitocal 300-G and similar devices: HeatPump class: - COP methods: getCoefficientOfPerformance{Heating,DHW,Total,Cooling,Green} - Compressor: getPower, getModulation (with units) - Refrigerant sensors: getHotGas/SuctionGas pressure and temperature, getLiquidGasTemperature - Runtime: getMainECURuntime, getHeatingRodRuntimeLevel{One,Two} - Configuration: buffer temp max, damping factor, heater approvals - Heating rod power consumption summary (DHW and heating) HeatingDevice class: - Primary circuit pump: getPrimaryCircuitPumpRotation (with unit) Compressor class: - Load class methods now support fallback to statistics.load path - Sensor methods: getInlet/Outlet/Overheat temperature, getInletPressure New CoolingCircuit class: - getType, getReverseActive Closes #677 * Remove deprecated COP green and split out CoolingCircuit - Remove getCoefficientOfPerformanceGreen (heating.cop.green is deprecated, replaced by heating.cop.photovoltaic) - Move heating.cop.green to deprecated properties list - Remove CoolingCircuit class (will be in separate PR) * Refactor heating rod methods into HeatingRod component class Extract all getHeatingRod* methods from HeatPump into a dedicated HeatingRod class, consistent with Compressor/Condensor/Inverter. Access via device.heatingRod.getStarts() etc. * Address review: revert HeatingRod class, add missing types - Revert HeatingRod component class back to direct methods on HeatPump (preserves existing getHeatingRod* API for backwards compatibility) - Add return type hints to getPrimaryCircuitPumpRotation and getTargetTemperature - Resolve merge conflicts from #667 merge * Remove unnecessary blank lines in getTemperature method --------- Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
1 parent 8f17bef commit c50eca0

File tree

5 files changed

+467
-37
lines changed

5 files changed

+467
-37
lines changed

PyViCare/PyViCareHeatPump.py

Lines changed: 220 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ def getInverter(self, inverter) -> Inverter:
4545

4646
@handleNotSupported
4747
def getBufferMainTemperature(self):
48-
return self.getProperty("heating.bufferCylinder.sensors.temperature.main")["properties"]['value']['value']
48+
return self.getProperty("heating.bufferCylinder.sensors.temperature.main")["properties"]["value"]["value"]
4949

5050
@handleNotSupported
5151
def getBufferTopTemperature(self):
52-
return self.getProperty("heating.bufferCylinder.sensors.temperature.top")["properties"]['value']['value']
52+
return self.getProperty("heating.bufferCylinder.sensors.temperature.top")["properties"]["value"]["value"]
5353

5454
# Power consumption for Heating:
5555
@handleNotSupported
@@ -325,6 +325,24 @@ def getSeasonalPerformanceFactorHeating(self) -> float:
325325
def getSeasonalPerformanceFactorTotal(self) -> float:
326326
return float(self.getProperty("heating.spf.total")["properties"]["value"]["value"])
327327

328+
# COP (Coefficient of Performance) - instantaneous efficiency metrics
329+
# Some devices expose COP instead of SPF
330+
@handleNotSupported
331+
def getCoefficientOfPerformanceHeating(self) -> float:
332+
return float(self.getProperty("heating.cop.heating")["properties"]["value"]["value"])
333+
334+
@handleNotSupported
335+
def getCoefficientOfPerformanceDHW(self) -> float:
336+
return float(self.getProperty("heating.cop.dhw")["properties"]["value"]["value"])
337+
338+
@handleNotSupported
339+
def getCoefficientOfPerformanceTotal(self) -> float:
340+
return float(self.getProperty("heating.cop.total")["properties"]["value"]["value"])
341+
342+
@handleNotSupported
343+
def getCoefficientOfPerformanceCooling(self) -> float:
344+
return float(self.getProperty("heating.cop.cooling")["properties"]["value"]["value"])
345+
328346
@handleNotSupported
329347
def getHeatingRodStarts(self) -> int:
330348
return int(self.getProperty("heating.heatingRod.statistics")["properties"]["starts"]["value"])
@@ -361,6 +379,157 @@ def getHeatingRodPowerConsumptionHeatingThisYear(self) -> float:
361379
def getHeatingRodPowerConsumptionTotalThisYear(self) -> float:
362380
return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0])
363381

382+
# Heating rod power consumption summary for DHW:
383+
@handleNotSupported
384+
def getHeatingRodPowerConsumptionSummaryDHWUnit(self) -> str:
385+
return str(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["unit"])
386+
387+
@handleNotSupported
388+
def getHeatingRodPowerConsumptionSummaryDHWCurrentDay(self) -> float:
389+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["value"])
390+
391+
@handleNotSupported
392+
def getHeatingRodPowerConsumptionSummaryDHWCurrentMonth(self) -> float:
393+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentMonth"]["value"])
394+
395+
@handleNotSupported
396+
def getHeatingRodPowerConsumptionSummaryDHWCurrentYear(self) -> float:
397+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentYear"]["value"])
398+
399+
@handleNotSupported
400+
def getHeatingRodPowerConsumptionSummaryDHWLastMonth(self) -> float:
401+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastMonth"]["value"])
402+
403+
@handleNotSupported
404+
def getHeatingRodPowerConsumptionSummaryDHWLastSevenDays(self) -> float:
405+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastSevenDays"]["value"])
406+
407+
@handleNotSupported
408+
def getHeatingRodPowerConsumptionSummaryDHWLastYear(self) -> float:
409+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastYear"]["value"])
410+
411+
# Heating rod power consumption summary for Heating:
412+
@handleNotSupported
413+
def getHeatingRodPowerConsumptionSummaryHeatingUnit(self) -> str:
414+
return str(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["unit"])
415+
416+
@handleNotSupported
417+
def getHeatingRodPowerConsumptionSummaryHeatingCurrentDay(self) -> float:
418+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["value"])
419+
420+
@handleNotSupported
421+
def getHeatingRodPowerConsumptionSummaryHeatingCurrentMonth(self) -> float:
422+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentMonth"]["value"])
423+
424+
@handleNotSupported
425+
def getHeatingRodPowerConsumptionSummaryHeatingCurrentYear(self) -> float:
426+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentYear"]["value"])
427+
428+
@handleNotSupported
429+
def getHeatingRodPowerConsumptionSummaryHeatingLastMonth(self) -> float:
430+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastMonth"]["value"])
431+
432+
@handleNotSupported
433+
def getHeatingRodPowerConsumptionSummaryHeatingLastSevenDays(self) -> float:
434+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastSevenDays"]["value"])
435+
436+
@handleNotSupported
437+
def getHeatingRodPowerConsumptionSummaryHeatingLastYear(self) -> float:
438+
return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastYear"]["value"])
439+
440+
# Heating rod runtime by level
441+
@handleNotSupported
442+
def getHeatingRodRuntimeLevelOne(self) -> int:
443+
return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["value"])
444+
445+
@handleNotSupported
446+
def getHeatingRodRuntimeLevelTwo(self) -> int:
447+
return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelTwo"]["value"])
448+
449+
@handleNotSupported
450+
def getHeatingRodRuntimeLevelOneUnit(self) -> str:
451+
return str(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["unit"])
452+
453+
# Additional pressure sensors (refrigerant circuit)
454+
@handleNotSupported
455+
def getHotGasPressure(self) -> float:
456+
return float(self.getProperty("heating.sensors.pressure.hotGas")["properties"]["value"]["value"])
457+
458+
@handleNotSupported
459+
def getHotGasPressureUnit(self) -> str:
460+
return str(self.getProperty("heating.sensors.pressure.hotGas")["properties"]["value"]["unit"])
461+
462+
@handleNotSupported
463+
def getSuctionGasPressure(self) -> float:
464+
return float(self.getProperty("heating.sensors.pressure.suctionGas")["properties"]["value"]["value"])
465+
466+
@handleNotSupported
467+
def getSuctionGasPressureUnit(self) -> str:
468+
return str(self.getProperty("heating.sensors.pressure.suctionGas")["properties"]["value"]["unit"])
469+
470+
# Additional temperature sensors (refrigerant circuit)
471+
@handleNotSupported
472+
def getHotGasTemperature(self) -> float:
473+
return float(self.getProperty("heating.sensors.temperature.hotGas")["properties"]["value"]["value"])
474+
475+
@handleNotSupported
476+
def getHotGasTemperatureUnit(self) -> str:
477+
return str(self.getProperty("heating.sensors.temperature.hotGas")["properties"]["value"]["unit"])
478+
479+
@handleNotSupported
480+
def getLiquidGasTemperature(self) -> float:
481+
return float(self.getProperty("heating.sensors.temperature.liquidGas")["properties"]["value"]["value"])
482+
483+
@handleNotSupported
484+
def getLiquidGasTemperatureUnit(self) -> str:
485+
return str(self.getProperty("heating.sensors.temperature.liquidGas")["properties"]["value"]["unit"])
486+
487+
@handleNotSupported
488+
def getSuctionGasTemperature(self) -> float:
489+
return float(self.getProperty("heating.sensors.temperature.suctionGas")["properties"]["value"]["value"])
490+
491+
@handleNotSupported
492+
def getSuctionGasTemperatureUnit(self) -> str:
493+
return str(self.getProperty("heating.sensors.temperature.suctionGas")["properties"]["value"]["unit"])
494+
495+
# Main ECU runtime
496+
@handleNotSupported
497+
def getMainECURuntime(self) -> int:
498+
return int(self.getProperty("heating.device.mainECU")["properties"]["runtime"]["value"])
499+
500+
@handleNotSupported
501+
def getMainECURuntimeUnit(self) -> str:
502+
return str(self.getProperty("heating.device.mainECU")["properties"]["runtime"]["unit"])
503+
504+
# Configuration values
505+
@handleNotSupported
506+
def getConfigurationBufferTemperatureMax(self) -> float:
507+
return float(self.getProperty("heating.configuration.buffer.temperature.max")["properties"]["value"]["value"])
508+
509+
@handleNotSupported
510+
def getConfigurationBufferTemperatureMaxUnit(self) -> str:
511+
return str(self.getProperty("heating.configuration.buffer.temperature.max")["properties"]["value"]["unit"])
512+
513+
@handleNotSupported
514+
def getConfigurationOutsideTemperatureDampingFactor(self) -> int:
515+
return int(self.getProperty("heating.configuration.temperature.outside.DampingFactor")["properties"]["value"]["value"])
516+
517+
@handleNotSupported
518+
def getConfigurationOutsideTemperatureDampingFactorUnit(self) -> str:
519+
return str(self.getProperty("heating.configuration.temperature.outside.DampingFactor")["properties"]["value"]["unit"])
520+
521+
@handleNotSupported
522+
def getConfigurationHeatingRodDHWApproved(self) -> bool:
523+
return bool(self.getProperty("heating.configuration.heatingRod.dhw")["properties"]["useApproved"]["value"])
524+
525+
@handleNotSupported
526+
def getConfigurationHeatingRodHeatingApproved(self) -> bool:
527+
return bool(self.getProperty("heating.configuration.heatingRod.heating")["properties"]["useApproved"]["value"])
528+
529+
@handleNotSupported
530+
def getConfigurationDHWHeaterApproved(self) -> bool:
531+
return bool(self.getProperty("heating.configuration.dhwHeater")["properties"]["useApproved"]["value"])
532+
364533
# Cooling circuits
365534
@property
366535
def coolingCircuits(self) -> List[CoolingCircuit]:
@@ -395,6 +564,7 @@ def getReverseActive(self) -> bool:
395564
return bool(self.getProperty(f"heating.coolingCircuits.{self.circuit}.reverse")["properties"]["active"]["value"])
396565

397566

567+
398568
class Compressor(HeatingDeviceWithComponent):
399569

400570
@property
@@ -409,25 +579,45 @@ def getStarts(self):
409579
def getHours(self):
410580
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hours"]["value"]
411581

412-
@handleNotSupported
413582
def getHoursLoadClass1(self):
414-
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassOne"]["value"]
583+
"""Get hours in load class 1. Tries 'statistics' path first, then 'statistics.load'."""
584+
with suppress(KeyError):
585+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassOne"]["value"]
586+
with suppress(KeyError):
587+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassOne"]["value"]
588+
raise PyViCareNotSupportedFeatureError("getHoursLoadClass1")
415589

416-
@handleNotSupported
417590
def getHoursLoadClass2(self):
418-
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassTwo"]["value"]
591+
"""Get hours in load class 2. Tries 'statistics' path first, then 'statistics.load'."""
592+
with suppress(KeyError):
593+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassTwo"]["value"]
594+
with suppress(KeyError):
595+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassTwo"]["value"]
596+
raise PyViCareNotSupportedFeatureError("getHoursLoadClass2")
419597

420-
@handleNotSupported
421598
def getHoursLoadClass3(self):
422-
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassThree"]["value"]
599+
"""Get hours in load class 3. Tries 'statistics' path first, then 'statistics.load'."""
600+
with suppress(KeyError):
601+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassThree"]["value"]
602+
with suppress(KeyError):
603+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassThree"]["value"]
604+
raise PyViCareNotSupportedFeatureError("getHoursLoadClass3")
423605

424-
@handleNotSupported
425606
def getHoursLoadClass4(self):
426-
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFour"]["value"]
607+
"""Get hours in load class 4. Tries 'statistics' path first, then 'statistics.load'."""
608+
with suppress(KeyError):
609+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFour"]["value"]
610+
with suppress(KeyError):
611+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassFour"]["value"]
612+
raise PyViCareNotSupportedFeatureError("getHoursLoadClass4")
427613

428-
@handleNotSupported
429614
def getHoursLoadClass5(self):
430-
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFive"]["value"]
615+
"""Get hours in load class 5. Tries 'statistics' path first, then 'statistics.load'."""
616+
with suppress(KeyError):
617+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFive"]["value"]
618+
with suppress(KeyError):
619+
return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassFive"]["value"]
620+
raise PyViCareNotSupportedFeatureError("getHoursLoadClass5")
431621

432622
@handleNotSupported
433623
def getActive(self):
@@ -437,6 +627,24 @@ def getActive(self):
437627
def getPhase(self):
438628
return self.getProperty(f"heating.compressors.{self.compressor}")["properties"]["phase"]["value"]
439629

630+
@handleNotSupported
631+
def getPower(self) -> float:
632+
# Returns the nominal/maximum power of the compressor in kW
633+
return float(self.getProperty(f"heating.compressors.{self.compressor}.power")["properties"]["value"]["value"])
634+
635+
@handleNotSupported
636+
def getPowerUnit(self) -> str:
637+
return str(self.getProperty(f"heating.compressors.{self.compressor}.power")["properties"]["value"]["unit"])
638+
639+
@handleNotSupported
640+
def getModulation(self) -> int:
641+
# Returns the current compressor modulation/power level as percentage (0-100)
642+
return int(self.getProperty(f"heating.compressors.{self.compressor}.sensors.power")["properties"]["value"]["value"])
643+
644+
@handleNotSupported
645+
def getModulationUnit(self) -> str:
646+
return str(self.getProperty(f"heating.compressors.{self.compressor}.sensors.power")["properties"]["value"]["unit"])
647+
440648
@handleNotSupported
441649
def getSpeed(self) -> int:
442650
return int(self.getProperty(f"heating.compressors.{self.compressor}.speed.current")["properties"]["value"]["value"])

PyViCare/PyViCareHeatingDevice.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,15 @@ def getReturnTemperaturePrimaryCircuit(self):
368368
return self.getProperty("heating.primaryCircuit.sensors.temperature.return")["properties"]["value"][
369369
"value"]
370370

371+
@handleNotSupported
372+
def getPrimaryCircuitPumpRotation(self) -> float:
373+
"""Get primary circuit pump rotation/speed as percentage."""
374+
return float(self.getProperty("heating.primaryCircuit.sensors.rotation")["properties"]["value"]["value"])
375+
376+
@handleNotSupported
377+
def getPrimaryCircuitPumpRotationUnit(self):
378+
return self.getProperty("heating.primaryCircuit.sensors.rotation")["properties"]["value"]["unit"]
379+
371380
@handleNotSupported
372381
def getSupplyTemperatureSecondaryCircuit(self):
373382
return self.getProperty("heating.secondaryCircuit.sensors.temperature.supply")["properties"]["value"][
@@ -548,6 +557,15 @@ def getSupplyTemperature(self):
548557
self.getProperty(f"heating.circuits.{self.circuit}.sensors.temperature.supply")["properties"][
549558
"value"]["value"]
550559

560+
@handleNotSupported
561+
def getTargetTemperature(self) -> float:
562+
"""Get the circuit target temperature."""
563+
return float(self.getProperty(f"heating.circuits.{self.circuit}.temperature")["properties"]["value"]["value"])
564+
565+
@handleNotSupported
566+
def getTargetTemperatureUnit(self):
567+
return self.getProperty(f"heating.circuits.{self.circuit}.temperature")["properties"]["value"]["unit"]
568+
551569
@handleNotSupported
552570
def getRoomTemperature(self):
553571
return self.getProperty(f"heating.circuits.{self.circuit}.sensors.temperature.room")["properties"][

tests/test_TestForMissingProperties.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ def test_deprecatedProperties(self):
3636
'ventilation.operating.programs.levelTwo',
3737
'ventilation.operating.programs.forcedLevelFour',
3838
'ventilation.operating.programs.silent',
39+
# Alternative naming conventions used as fallback for device compatibility
40+
'heating.buffer.sensors.temperature.main',
41+
'heating.buffer.sensors.temperature.top',
42+
'heating.dhw.sensors.temperature.hotWaterStorage',
43+
'heating.dhw.sensors.temperature.hotWaterStorage.top',
44+
'heating.dhw.sensors.temperature.hotWaterStorage.bottom',
45+
'heating.dhw.sensors.temperature.hotWaterStorage.middle',
46+
'heating.dhw.sensors.temperature.hotWaterStorage.midBottom',
47+
'heating.cop.green', # deprecated, replaced by heating.cop.photovoltaic
3948
]
4049

4150
all_features = self.read_all_deprecated_features()
@@ -201,12 +210,11 @@ def test_missingProperties(self):
201210
'heating.compressors.0.sensors.power',
202211
'heating.compressors.0.statistics.load',
203212
'heating.configuration.buffer.temperature.max',
204-
'heating.configuration.dhwHeater',
205213
'heating.configuration.flow.temperature.max',
206214
'heating.configuration.flow.temperature.min',
207215
'heating.cop.cooling',
208216
'heating.cop.dhw',
209-
'heating.cop.green',
217+
'heating.cop.green', # deprecated, replaced by heating.cop.photovoltaic
210218
'heating.cop.heating',
211219
'heating.cop.total',
212220
'heating.heatingRod.heatTarget',
@@ -217,8 +225,6 @@ def test_missingProperties(self):
217225
'heating.sensors.temperature.hotGas',
218226
'heating.sensors.temperature.liquidGas',
219227
'heating.sensors.temperature.suctionGas',
220-
'heating.heatingRod.power.consumption.summary.dhw',
221-
'heating.heatingRod.power.consumption.summary.heating',
222228
'heating.heatingRod.status',
223229
'heating.scop.dhw', # deprecated
224230
'heating.scop.heating', # deprecated
@@ -362,6 +368,7 @@ def test_unverifiedProperties(self):
362368
for match in re.findall(r'getProperty\(\s*?f?"(.*)"\s*?\)', all_python_files[python]):
363369
feature_name = re.sub(r'{(self\.)?(circuit|burner|compressor|condensor|evaporator|inverter)}', '0', match)
364370
feature_name = re.sub(r'{burner}', '0', feature_name)
371+
feature_name = re.sub(r'{circuit}', '0', feature_name) # for local variable in loops
365372
feature_name = re.sub(r'\.{(quickmode|mode|program|active_program)}', '', feature_name)
366373
used_features.append(feature_name)
367374

0 commit comments

Comments
 (0)