From 6fffb4ea7f67a0d0f01ac6b9751418e35951bc6e Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Mon, 2 Feb 2026 14:11:03 +0100 Subject: [PATCH 1/5] 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 --- PyViCare/PyViCareHeatPump.py | 235 +++++++++++++++++++++++-- PyViCare/PyViCareHeatingDevice.py | 18 ++ tests/test_TestForMissingProperties.py | 12 +- tests/test_Vitocal250A.py | 32 ++++ tests/test_Vitocal300G.py | 207 ++++++++++++++++++++-- 5 files changed, 472 insertions(+), 32 deletions(-) diff --git a/PyViCare/PyViCareHeatPump.py b/PyViCare/PyViCareHeatPump.py index 36701ed9..61ad3c16 100644 --- a/PyViCare/PyViCareHeatPump.py +++ b/PyViCare/PyViCareHeatPump.py @@ -45,11 +45,11 @@ def getInverter(self, inverter) -> Inverter: @handleNotSupported def getBufferMainTemperature(self): - return self.getProperty("heating.bufferCylinder.sensors.temperature.main")["properties"]['value']['value'] + return self.getProperty("heating.bufferCylinder.sensors.temperature.main")["properties"]["value"]["value"] @handleNotSupported def getBufferTopTemperature(self): - return self.getProperty("heating.bufferCylinder.sensors.temperature.top")["properties"]['value']['value'] + return self.getProperty("heating.bufferCylinder.sensors.temperature.top")["properties"]["value"]["value"] # Power consumption for Heating: @handleNotSupported @@ -325,6 +325,28 @@ def getSeasonalPerformanceFactorHeating(self) -> float: def getSeasonalPerformanceFactorTotal(self) -> float: return float(self.getProperty("heating.spf.total")["properties"]["value"]["value"]) + # COP (Coefficient of Performance) - instantaneous efficiency metrics + # Some devices expose COP instead of SPF + @handleNotSupported + def getCoefficientOfPerformanceHeating(self) -> float: + return float(self.getProperty("heating.cop.heating")["properties"]["value"]["value"]) + + @handleNotSupported + def getCoefficientOfPerformanceDHW(self) -> float: + return float(self.getProperty("heating.cop.dhw")["properties"]["value"]["value"]) + + @handleNotSupported + def getCoefficientOfPerformanceTotal(self) -> float: + return float(self.getProperty("heating.cop.total")["properties"]["value"]["value"]) + + @handleNotSupported + def getCoefficientOfPerformanceCooling(self) -> float: + return float(self.getProperty("heating.cop.cooling")["properties"]["value"]["value"]) + + @handleNotSupported + def getCoefficientOfPerformanceGreen(self) -> float: + return float(self.getProperty("heating.cop.green")["properties"]["value"]["value"]) + @handleNotSupported def getHeatingRodStarts(self) -> int: return int(self.getProperty("heating.heatingRod.statistics")["properties"]["starts"]["value"]) @@ -361,6 +383,157 @@ def getHeatingRodPowerConsumptionHeatingThisYear(self) -> float: def getHeatingRodPowerConsumptionTotalThisYear(self) -> float: return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0]) + # Heating rod power consumption summary for DHW: + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["unit"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWCurrentDay(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWCurrentMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWCurrentYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentYear"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWLastMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWLastSevenDays(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastSevenDays"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWLastYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastYear"]["value"]) + + # Heating rod power consumption summary for Heating: + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["unit"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingCurrentDay(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingCurrentMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingCurrentYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentYear"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingLastMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingLastSevenDays(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastSevenDays"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingLastYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastYear"]["value"]) + + # Heating rod runtime by level + @handleNotSupported + def getHeatingRodRuntimeLevelOne(self) -> int: + return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["value"]) + + @handleNotSupported + def getHeatingRodRuntimeLevelTwo(self) -> int: + return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelTwo"]["value"]) + + @handleNotSupported + def getHeatingRodRuntimeLevelOneUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["unit"]) + + # Additional pressure sensors (refrigerant circuit) + @handleNotSupported + def getHotGasPressure(self) -> float: + return float(self.getProperty("heating.sensors.pressure.hotGas")["properties"]["value"]["value"]) + + @handleNotSupported + def getHotGasPressureUnit(self) -> str: + return str(self.getProperty("heating.sensors.pressure.hotGas")["properties"]["value"]["unit"]) + + @handleNotSupported + def getSuctionGasPressure(self) -> float: + return float(self.getProperty("heating.sensors.pressure.suctionGas")["properties"]["value"]["value"]) + + @handleNotSupported + def getSuctionGasPressureUnit(self) -> str: + return str(self.getProperty("heating.sensors.pressure.suctionGas")["properties"]["value"]["unit"]) + + # Additional temperature sensors (refrigerant circuit) + @handleNotSupported + def getHotGasTemperature(self) -> float: + return float(self.getProperty("heating.sensors.temperature.hotGas")["properties"]["value"]["value"]) + + @handleNotSupported + def getHotGasTemperatureUnit(self) -> str: + return str(self.getProperty("heating.sensors.temperature.hotGas")["properties"]["value"]["unit"]) + + @handleNotSupported + def getLiquidGasTemperature(self) -> float: + return float(self.getProperty("heating.sensors.temperature.liquidGas")["properties"]["value"]["value"]) + + @handleNotSupported + def getLiquidGasTemperatureUnit(self) -> str: + return str(self.getProperty("heating.sensors.temperature.liquidGas")["properties"]["value"]["unit"]) + + @handleNotSupported + def getSuctionGasTemperature(self) -> float: + return float(self.getProperty("heating.sensors.temperature.suctionGas")["properties"]["value"]["value"]) + + @handleNotSupported + def getSuctionGasTemperatureUnit(self) -> str: + return str(self.getProperty("heating.sensors.temperature.suctionGas")["properties"]["value"]["unit"]) + + # Main ECU runtime + @handleNotSupported + def getMainECURuntime(self) -> int: + return int(self.getProperty("heating.device.mainECU")["properties"]["runtime"]["value"]) + + @handleNotSupported + def getMainECURuntimeUnit(self) -> str: + return str(self.getProperty("heating.device.mainECU")["properties"]["runtime"]["unit"]) + + # Configuration values + @handleNotSupported + def getConfigurationBufferTemperatureMax(self) -> float: + return float(self.getProperty("heating.configuration.buffer.temperature.max")["properties"]["value"]["value"]) + + @handleNotSupported + def getConfigurationBufferTemperatureMaxUnit(self) -> str: + return str(self.getProperty("heating.configuration.buffer.temperature.max")["properties"]["value"]["unit"]) + + @handleNotSupported + def getConfigurationOutsideTemperatureDampingFactor(self) -> int: + return int(self.getProperty("heating.configuration.temperature.outside.DampingFactor")["properties"]["value"]["value"]) + + @handleNotSupported + def getConfigurationOutsideTemperatureDampingFactorUnit(self) -> str: + return str(self.getProperty("heating.configuration.temperature.outside.DampingFactor")["properties"]["value"]["unit"]) + + @handleNotSupported + def getConfigurationHeatingRodDHWApproved(self) -> bool: + return bool(self.getProperty("heating.configuration.heatingRod.dhw")["properties"]["useApproved"]["value"]) + + @handleNotSupported + def getConfigurationHeatingRodHeatingApproved(self) -> bool: + return bool(self.getProperty("heating.configuration.heatingRod.heating")["properties"]["useApproved"]["value"]) + + @handleNotSupported + def getConfigurationDHWHeaterApproved(self) -> bool: + return bool(self.getProperty("heating.configuration.dhwHeater")["properties"]["useApproved"]["value"]) + # Cooling circuits @property def coolingCircuits(self) -> List[CoolingCircuit]: @@ -409,25 +582,45 @@ def getStarts(self): def getHours(self): return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hours"]["value"] - @handleNotSupported def getHoursLoadClass1(self): - return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassOne"]["value"] + """Get hours in load class 1. Tries 'statistics' path first, then 'statistics.load'.""" + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassOne"]["value"] + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassOne"]["value"] + raise PyViCareNotSupportedFeatureError("getHoursLoadClass1") - @handleNotSupported def getHoursLoadClass2(self): - return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassTwo"]["value"] + """Get hours in load class 2. Tries 'statistics' path first, then 'statistics.load'.""" + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassTwo"]["value"] + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassTwo"]["value"] + raise PyViCareNotSupportedFeatureError("getHoursLoadClass2") - @handleNotSupported def getHoursLoadClass3(self): - return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassThree"]["value"] + """Get hours in load class 3. Tries 'statistics' path first, then 'statistics.load'.""" + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassThree"]["value"] + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassThree"]["value"] + raise PyViCareNotSupportedFeatureError("getHoursLoadClass3") - @handleNotSupported def getHoursLoadClass4(self): - return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFour"]["value"] + """Get hours in load class 4. Tries 'statistics' path first, then 'statistics.load'.""" + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFour"]["value"] + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassFour"]["value"] + raise PyViCareNotSupportedFeatureError("getHoursLoadClass4") - @handleNotSupported def getHoursLoadClass5(self): - return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFive"]["value"] + """Get hours in load class 5. Tries 'statistics' path first, then 'statistics.load'.""" + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics")["properties"]["hoursLoadClassFive"]["value"] + with suppress(KeyError): + return self.getProperty(f"heating.compressors.{self.compressor}.statistics.load")["properties"]["hoursLoadClassFive"]["value"] + raise PyViCareNotSupportedFeatureError("getHoursLoadClass5") @handleNotSupported def getActive(self): @@ -437,6 +630,24 @@ def getActive(self): def getPhase(self): return self.getProperty(f"heating.compressors.{self.compressor}")["properties"]["phase"]["value"] + @handleNotSupported + def getPower(self) -> float: + # Returns the nominal/maximum power of the compressor in kW + return float(self.getProperty(f"heating.compressors.{self.compressor}.power")["properties"]["value"]["value"]) + + @handleNotSupported + def getPowerUnit(self) -> str: + return str(self.getProperty(f"heating.compressors.{self.compressor}.power")["properties"]["value"]["unit"]) + + @handleNotSupported + def getModulation(self) -> int: + # Returns the current compressor modulation/power level as percentage (0-100) + return int(self.getProperty(f"heating.compressors.{self.compressor}.sensors.power")["properties"]["value"]["value"]) + + @handleNotSupported + def getModulationUnit(self) -> str: + return str(self.getProperty(f"heating.compressors.{self.compressor}.sensors.power")["properties"]["value"]["unit"]) + @handleNotSupported def getSpeed(self) -> int: return int(self.getProperty(f"heating.compressors.{self.compressor}.speed.current")["properties"]["value"]["value"]) diff --git a/PyViCare/PyViCareHeatingDevice.py b/PyViCare/PyViCareHeatingDevice.py index 4cd12d8e..de31df05 100644 --- a/PyViCare/PyViCareHeatingDevice.py +++ b/PyViCare/PyViCareHeatingDevice.py @@ -368,6 +368,15 @@ def getReturnTemperaturePrimaryCircuit(self): return self.getProperty("heating.primaryCircuit.sensors.temperature.return")["properties"]["value"][ "value"] + @handleNotSupported + def getPrimaryCircuitPumpRotation(self) -> float: + """Get primary circuit pump rotation/speed as percentage.""" + return float(self.getProperty("heating.primaryCircuit.sensors.rotation")["properties"]["value"]["value"]) + + @handleNotSupported + def getPrimaryCircuitPumpRotationUnit(self): + return self.getProperty("heating.primaryCircuit.sensors.rotation")["properties"]["value"]["unit"] + @handleNotSupported def getSupplyTemperatureSecondaryCircuit(self): return self.getProperty("heating.secondaryCircuit.sensors.temperature.supply")["properties"]["value"][ @@ -548,6 +557,15 @@ def getSupplyTemperature(self): self.getProperty(f"heating.circuits.{self.circuit}.sensors.temperature.supply")["properties"][ "value"]["value"] + @handleNotSupported + def getTargetTemperature(self) -> float: + """Get the circuit target temperature.""" + return float(self.getProperty(f"heating.circuits.{self.circuit}.temperature")["properties"]["value"]["value"]) + + @handleNotSupported + def getTargetTemperatureUnit(self): + return self.getProperty(f"heating.circuits.{self.circuit}.temperature")["properties"]["value"]["unit"] + @handleNotSupported def getRoomTemperature(self): return self.getProperty(f"heating.circuits.{self.circuit}.sensors.temperature.room")["properties"][ diff --git a/tests/test_TestForMissingProperties.py b/tests/test_TestForMissingProperties.py index 7b06e748..f7dae302 100644 --- a/tests/test_TestForMissingProperties.py +++ b/tests/test_TestForMissingProperties.py @@ -36,6 +36,14 @@ def test_deprecatedProperties(self): 'ventilation.operating.programs.levelTwo', 'ventilation.operating.programs.forcedLevelFour', 'ventilation.operating.programs.silent', + # Alternative naming conventions used as fallback for device compatibility + 'heating.buffer.sensors.temperature.main', + 'heating.buffer.sensors.temperature.top', + 'heating.dhw.sensors.temperature.hotWaterStorage', + 'heating.dhw.sensors.temperature.hotWaterStorage.top', + 'heating.dhw.sensors.temperature.hotWaterStorage.bottom', + 'heating.dhw.sensors.temperature.hotWaterStorage.middle', + 'heating.dhw.sensors.temperature.hotWaterStorage.midBottom', ] all_features = self.read_all_deprecated_features() @@ -201,7 +209,6 @@ def test_missingProperties(self): 'heating.compressors.0.sensors.power', 'heating.compressors.0.statistics.load', 'heating.configuration.buffer.temperature.max', - 'heating.configuration.dhwHeater', 'heating.configuration.flow.temperature.max', 'heating.configuration.flow.temperature.min', 'heating.cop.cooling', @@ -217,8 +224,6 @@ def test_missingProperties(self): 'heating.sensors.temperature.hotGas', 'heating.sensors.temperature.liquidGas', 'heating.sensors.temperature.suctionGas', - 'heating.heatingRod.power.consumption.summary.dhw', - 'heating.heatingRod.power.consumption.summary.heating', 'heating.heatingRod.status', 'heating.scop.dhw', # deprecated 'heating.scop.heating', # deprecated @@ -362,6 +367,7 @@ def test_unverifiedProperties(self): for match in re.findall(r'getProperty\(\s*?f?"(.*)"\s*?\)', all_python_files[python]): feature_name = re.sub(r'{(self\.)?(circuit|burner|compressor|condensor|evaporator|inverter)}', '0', match) feature_name = re.sub(r'{burner}', '0', feature_name) + feature_name = re.sub(r'{circuit}', '0', feature_name) # for local variable in loops feature_name = re.sub(r'\.{(quickmode|mode|program|active_program)}', '', feature_name) used_features.append(feature_name) diff --git a/tests/test_Vitocal250A.py b/tests/test_Vitocal250A.py index 227ab56e..2573bb8a 100644 --- a/tests/test_Vitocal250A.py +++ b/tests/test_Vitocal250A.py @@ -233,6 +233,38 @@ def test_getHeatingRod(self): self.assertEqual(self.device.getHeatingRodStarts(), 314) self.assertEqual(self.device.getHeatingRodHours(), 31) + def test_getHeatingRodPowerConsumptionSummaryDHW(self): + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryDHWUnit(), "kilowattHour") + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentDay(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentMonth(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentYear(), 3.3) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryDHWLastMonth(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryDHWLastSevenDays(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryDHWLastYear(), 28) + + def test_getHeatingRodPowerConsumptionSummaryHeating(self): + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryHeatingUnit(), "kilowattHour") + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentDay(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentMonth(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentYear(), 29.5) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryHeatingLastMonth(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryHeatingLastSevenDays(), 0) + self.assertEqual( + self.device.getHeatingRodPowerConsumptionSummaryHeatingLastYear(), 53.6) + def test_inverter_getCurrent(self): self.assertEqual(self.device.inverters[0].getCurrent(), 0) diff --git a/tests/test_Vitocal300G.py b/tests/test_Vitocal300G.py index 59434471..470c6b23 100644 --- a/tests/test_Vitocal300G.py +++ b/tests/test_Vitocal300G.py @@ -9,9 +9,51 @@ def setUp(self): self.service = ViCareServiceMock('response/Vitocal300G_CU401B.json') self.device = HeatPump(self.service) + # COP (Coefficient of Performance) tests + def test_getCoefficientOfPerformanceHeating(self): + self.assertAlmostEqual( + self.device.getCoefficientOfPerformanceHeating(), 4.8) + + def test_getCoefficientOfPerformanceDHW(self): + self.assertAlmostEqual( + self.device.getCoefficientOfPerformanceDHW(), 4.0) + + def test_getCoefficientOfPerformanceTotal(self): + self.assertAlmostEqual( + self.device.getCoefficientOfPerformanceTotal(), 4.7) + + def test_getCoefficientOfPerformanceCooling(self): + self.assertAlmostEqual( + self.device.getCoefficientOfPerformanceCooling(), 0.0) + + def test_getCoefficientOfPerformanceGreen(self): + self.assertAlmostEqual( + self.device.getCoefficientOfPerformanceGreen(), 0.0) + + # Compressor power tests + def test_compressor_getPower(self): + self.assertAlmostEqual( + self.device.compressors[0].getPower(), 12.0) + + def test_compressor_getPowerUnit(self): + self.assertEqual( + self.device.compressors[0].getPowerUnit(), "kilowatt") + + def test_compressor_getModulation(self): + self.assertEqual( + self.device.compressors[0].getModulation(), 100) + + def test_compressor_getModulationUnit(self): + self.assertEqual( + self.device.compressors[0].getModulationUnit(), "percent") + + # Compressor statistics tests def test_compressor_getActive(self): self.assertEqual(self.device.compressors[0].getActive(), True) + def test_compressor_getPhase(self): + self.assertEqual(self.device.compressors[0].getPhase(), "heating") + def test_compressor_getHours(self): self.assertAlmostEqual( self.device.compressors[0].getHours(), 942.4) @@ -20,66 +62,197 @@ def test_compressor_getStarts(self): self.assertAlmostEqual( self.device.compressors[0].getStarts(), 363) - # Load class tests require fallback logic from PR #689 - # Data is in statistics.load instead of statistics - @unittest.skip("Requires PR #689 for statistics.load fallback") + # Load class tests - use statistics.load fallback path def test_compressor_getHoursLoadClass1(self): self.assertAlmostEqual( self.device.compressors[0].getHoursLoadClass1(), 5) - @unittest.skip("Requires PR #689 for statistics.load fallback") def test_compressor_getHoursLoadClass2(self): self.assertAlmostEqual( self.device.compressors[0].getHoursLoadClass2(), 233) - @unittest.skip("Requires PR #689 for statistics.load fallback") def test_compressor_getHoursLoadClass3(self): self.assertAlmostEqual( self.device.compressors[0].getHoursLoadClass3(), 448) - @unittest.skip("Requires PR #689 for statistics.load fallback") def test_compressor_getHoursLoadClass4(self): self.assertAlmostEqual( self.device.compressors[0].getHoursLoadClass4(), 249) - @unittest.skip("Requires PR #689 for statistics.load fallback") def test_compressor_getHoursLoadClass5(self): self.assertAlmostEqual( self.device.compressors[0].getHoursLoadClass5(), 3) - # This device only has circuit "1" enabled (circuits[0] in the list) - def test_getHeatingCurveSlope(self): + # Compressor sensor tests + def test_compressor_getInletPressure(self): self.assertAlmostEqual( - self.device.circuits[0].getHeatingCurveSlope(), 1.0) + self.device.compressors[0].getInletPressure(), 8.7) - def test_getHeatingCurveShift(self): + def test_compressor_getInletTemperature(self): self.assertAlmostEqual( - self.device.circuits[0].getHeatingCurveShift(), 2) + self.device.compressors[0].getInletTemperature(), 7.1) + + def test_compressor_getOutletTemperature(self): + self.assertAlmostEqual( + self.device.compressors[0].getOutletTemperature(), 79.5) + + def test_compressor_getOverheatTemperature(self): + self.assertAlmostEqual( + self.device.compressors[0].getOverheatTemperature(), 4.0) + + # General device tests + def test_getOutsideTemperature(self): + self.assertAlmostEqual( + self.device.getOutsideTemperature(), 4.1) def test_getReturnTemperature(self): self.assertAlmostEqual(self.device.getReturnTemperature(), 35.8) + def test_getSupplyTemperaturePrimaryCircuit(self): + self.assertAlmostEqual( + self.device.getSupplyTemperaturePrimaryCircuit(), 8.7) + def test_getReturnTemperaturePrimaryCircuit(self): self.assertAlmostEqual(self.device.getReturnTemperaturePrimaryCircuit(), 4.9) - def test_getSupplyTemperaturePrimaryCircuit(self): + # DHW tests + def test_getDomesticHotWaterStorageTemperature(self): self.assertAlmostEqual( - self.device.getSupplyTemperaturePrimaryCircuit(), 8.7) + self.device.getDomesticHotWaterStorageTemperature(), 52.4) + + def test_getDomesticHotWaterConfiguredTemperature(self): + self.assertAlmostEqual( + self.device.getDomesticHotWaterConfiguredTemperature(), 50.0) + + def test_getDomesticHotWaterCirculationPumpActive(self): + self.assertEqual( + self.device.getDomesticHotWaterCirculationPumpActive(), False) + + def test_getHotWaterStorageTemperatureTop(self): + self.assertAlmostEqual( + self.device.getHotWaterStorageTemperatureTop(), 52.4) + + # Buffer tests + def test_getBufferMainTemperature(self): + self.assertAlmostEqual( + self.device.getBufferMainTemperature(), 36.2) + + def test_getBufferTopTemperature(self): + self.assertAlmostEqual( + self.device.getBufferTopTemperature(), 36.2) + + # Circuit tests + def test_circuit_getSupplyTemperature(self): + self.assertAlmostEqual( + self.device.circuits[0].getSupplyTemperature(), 36.1) + + def test_getHeatingCurveSlope(self): + self.assertAlmostEqual( + self.device.circuits[0].getHeatingCurveSlope(), 1.0) + + def test_getHeatingCurveShift(self): + self.assertAlmostEqual( + self.device.circuits[0].getHeatingCurveShift(), 2) + + def test_circuit_getActiveMode(self): + self.assertEqual( + self.device.circuits[0].getActiveMode(), "dhwAndHeating") + + def test_circuit_getActiveProgram(self): + self.assertEqual( + self.device.circuits[0].getActiveProgram(), "normal") + + def test_circuit_getTargetTemperature(self): + self.assertAlmostEqual( + self.device.circuits[0].getTargetTemperature(), 40) def test_getPrograms(self): expected_programs = ['comfort', 'eco', 'fixed', 'normal', 'reduced', 'standby'] self.assertListEqual( self.device.circuits[0].getPrograms(), expected_programs) - # Available modes vary by device configuration (e.g., cooling enabled) def test_getModes(self): expected_modes = ['dhw', 'dhwAndHeating', 'standby'] self.assertListEqual( self.device.circuits[0].getModes(), expected_modes) - def test_getDomesticHotWaterCirculationPumpActive(self): + # Primary circuit pump tests + def test_getPrimaryCircuitPumpRotation(self): + self.assertAlmostEqual( + self.device.getPrimaryCircuitPumpRotation(), 80) + + def test_getPrimaryCircuitPumpRotationUnit(self): self.assertEqual( - self.device.getDomesticHotWaterCirculationPumpActive(), False) + self.device.getPrimaryCircuitPumpRotationUnit(), "percent") + + # Pressure sensor tests (refrigerant circuit) + def test_getHotGasPressure(self): + self.assertAlmostEqual( + self.device.getHotGasPressure(), 28.1) + + def test_getHotGasPressureUnit(self): + self.assertEqual( + self.device.getHotGasPressureUnit(), "bar") + + def test_getSuctionGasPressure(self): + self.assertAlmostEqual( + self.device.getSuctionGasPressure(), 8.7) + + def test_getSuctionGasPressureUnit(self): + self.assertEqual( + self.device.getSuctionGasPressureUnit(), "bar") + + # Temperature sensor tests (refrigerant circuit) + def test_getHotGasTemperature(self): + self.assertAlmostEqual( + self.device.getHotGasTemperature(), 79.5) + + def test_getLiquidGasTemperature(self): + self.assertAlmostEqual( + self.device.getLiquidGasTemperature(), 36) + + def test_getSuctionGasTemperature(self): + self.assertAlmostEqual( + self.device.getSuctionGasTemperature(), 7.1) + + # Main ECU runtime test + def test_getMainECURuntime(self): + self.assertEqual( + self.device.getMainECURuntime(), 7768472) + + def test_getMainECURuntimeUnit(self): + self.assertEqual( + self.device.getMainECURuntimeUnit(), "seconds") + + # Heating rod runtime tests + def test_getHeatingRodRuntimeLevelOne(self): + self.assertEqual( + self.device.getHeatingRodRuntimeLevelOne(), 886682) + + def test_getHeatingRodRuntimeLevelTwo(self): + self.assertEqual( + self.device.getHeatingRodRuntimeLevelTwo(), 287877) + + # Configuration tests + def test_getConfigurationBufferTemperatureMax(self): + self.assertAlmostEqual( + self.device.getConfigurationBufferTemperatureMax(), 65) + + def test_getConfigurationOutsideTemperatureDampingFactor(self): + self.assertEqual( + self.device.getConfigurationOutsideTemperatureDampingFactor(), 180) + + def test_getConfigurationHeatingRodDHWApproved(self): + self.assertEqual( + self.device.getConfigurationHeatingRodDHWApproved(), False) + + def test_getConfigurationHeatingRodHeatingApproved(self): + self.assertEqual( + self.device.getConfigurationHeatingRodHeatingApproved(), False) + + def test_getConfigurationDHWHeaterApproved(self): + self.assertEqual( + self.device.getConfigurationDHWHeaterApproved(), True) # Cooling circuit tests def test_getAvailableCoolingCircuits(self): From 5455494e2708296377b37a8a4c48f7656d1e5748 Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Tue, 10 Feb 2026 10:38:41 +0100 Subject: [PATCH 2/5] 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) --- PyViCare/PyViCareHeatPump.py | 5 +---- tests/test_TestForMissingProperties.py | 3 ++- tests/test_Vitocal300G.py | 4 ---- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/PyViCare/PyViCareHeatPump.py b/PyViCare/PyViCareHeatPump.py index 61ad3c16..74da5477 100644 --- a/PyViCare/PyViCareHeatPump.py +++ b/PyViCare/PyViCareHeatPump.py @@ -343,10 +343,6 @@ def getCoefficientOfPerformanceTotal(self) -> float: def getCoefficientOfPerformanceCooling(self) -> float: return float(self.getProperty("heating.cop.cooling")["properties"]["value"]["value"]) - @handleNotSupported - def getCoefficientOfPerformanceGreen(self) -> float: - return float(self.getProperty("heating.cop.green")["properties"]["value"]["value"]) - @handleNotSupported def getHeatingRodStarts(self) -> int: return int(self.getProperty("heating.heatingRod.statistics")["properties"]["starts"]["value"]) @@ -568,6 +564,7 @@ def getReverseActive(self) -> bool: return bool(self.getProperty(f"heating.coolingCircuits.{self.circuit}.reverse")["properties"]["active"]["value"]) + class Compressor(HeatingDeviceWithComponent): @property diff --git a/tests/test_TestForMissingProperties.py b/tests/test_TestForMissingProperties.py index f7dae302..c6fa396f 100644 --- a/tests/test_TestForMissingProperties.py +++ b/tests/test_TestForMissingProperties.py @@ -44,6 +44,7 @@ def test_deprecatedProperties(self): 'heating.dhw.sensors.temperature.hotWaterStorage.bottom', 'heating.dhw.sensors.temperature.hotWaterStorage.middle', 'heating.dhw.sensors.temperature.hotWaterStorage.midBottom', + 'heating.cop.green', # deprecated, replaced by heating.cop.photovoltaic ] all_features = self.read_all_deprecated_features() @@ -213,7 +214,7 @@ def test_missingProperties(self): 'heating.configuration.flow.temperature.min', 'heating.cop.cooling', 'heating.cop.dhw', - 'heating.cop.green', + 'heating.cop.green', # deprecated, replaced by heating.cop.photovoltaic 'heating.cop.heating', 'heating.cop.total', 'heating.heatingRod.heatTarget', diff --git a/tests/test_Vitocal300G.py b/tests/test_Vitocal300G.py index 470c6b23..e603e6a5 100644 --- a/tests/test_Vitocal300G.py +++ b/tests/test_Vitocal300G.py @@ -26,10 +26,6 @@ def test_getCoefficientOfPerformanceCooling(self): self.assertAlmostEqual( self.device.getCoefficientOfPerformanceCooling(), 0.0) - def test_getCoefficientOfPerformanceGreen(self): - self.assertAlmostEqual( - self.device.getCoefficientOfPerformanceGreen(), 0.0) - # Compressor power tests def test_compressor_getPower(self): self.assertAlmostEqual( From 4363dd8590bbaf6d86e5c6085c8cfa7962040e55 Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Thu, 12 Feb 2026 09:49:50 +0100 Subject: [PATCH 3/5] 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. --- PyViCare/PyViCareHeatPump.py | 225 ++++++++++++++++++----------------- tests/test_Vitocal250A.py | 36 +++--- tests/test_Vitocal300G.py | 4 +- 3 files changed, 137 insertions(+), 128 deletions(-) diff --git a/PyViCare/PyViCareHeatPump.py b/PyViCare/PyViCareHeatPump.py index 74da5477..362926fb 100644 --- a/PyViCare/PyViCareHeatPump.py +++ b/PyViCare/PyViCareHeatPump.py @@ -343,112 +343,9 @@ def getCoefficientOfPerformanceTotal(self) -> float: def getCoefficientOfPerformanceCooling(self) -> float: return float(self.getProperty("heating.cop.cooling")["properties"]["value"]["value"]) - @handleNotSupported - def getHeatingRodStarts(self) -> int: - return int(self.getProperty("heating.heatingRod.statistics")["properties"]["starts"]["value"]) - - @handleNotSupported - def getHeatingRodHours(self) -> int: - return int(self.getProperty("heating.heatingRod.statistics")["properties"]["hours"]["value"]) - - @handleNotSupported - def getHeatingRodHeatProductionCurrent(self) -> float: - return float(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["value"]) - - @handleNotSupported - def getHeatingRodHeatProductionCurrentUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["unit"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionCurrent(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionCurrentUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["unit"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionDHWThisYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.dhw")["properties"]["year"]["value"][0]) - - @handleNotSupported - def getHeatingRodPowerConsumptionHeatingThisYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.heating")["properties"]["year"]["value"][0]) - - @handleNotSupported - def getHeatingRodPowerConsumptionTotalThisYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0]) - - # Heating rod power consumption summary for DHW: - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryDHWUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["unit"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryDHWCurrentDay(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryDHWCurrentMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentMonth"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryDHWCurrentYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentYear"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryDHWLastMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastMonth"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryDHWLastSevenDays(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastSevenDays"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryDHWLastYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastYear"]["value"]) - - # Heating rod power consumption summary for Heating: - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryHeatingUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["unit"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryHeatingCurrentDay(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryHeatingCurrentMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentMonth"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryHeatingCurrentYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentYear"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryHeatingLastMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastMonth"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryHeatingLastSevenDays(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastSevenDays"]["value"]) - - @handleNotSupported - def getHeatingRodPowerConsumptionSummaryHeatingLastYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastYear"]["value"]) - - # Heating rod runtime by level - @handleNotSupported - def getHeatingRodRuntimeLevelOne(self) -> int: - return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["value"]) - - @handleNotSupported - def getHeatingRodRuntimeLevelTwo(self) -> int: - return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelTwo"]["value"]) - - @handleNotSupported - def getHeatingRodRuntimeLevelOneUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["unit"]) + @property + def heatingRod(self) -> HeatingRod: + return HeatingRod(self) # Additional pressure sensors (refrigerant circuit) @handleNotSupported @@ -910,3 +807,119 @@ def getPower(self) -> float: @handleNotSupported def getTemperature(self) -> float: return float(self.getProperty(f"heating.inverters.{self.inverter}.sensors.temperature.powerModule")["properties"]["value"]["value"]) + + +class HeatingRod: + + def __init__(self, device: HeatPump) -> None: + self.service = device.service + + def getProperty(self, property_name: str) -> Any: + return self.service.getProperty(property_name) + + @handleNotSupported + def getStarts(self) -> int: + return int(self.getProperty("heating.heatingRod.statistics")["properties"]["starts"]["value"]) + + @handleNotSupported + def getHours(self) -> int: + return int(self.getProperty("heating.heatingRod.statistics")["properties"]["hours"]["value"]) + + @handleNotSupported + def getHeatProductionCurrent(self) -> float: + return float(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["value"]) + + @handleNotSupported + def getHeatProductionCurrentUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["unit"]) + + @handleNotSupported + def getPowerConsumptionCurrent(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["value"]) + + @handleNotSupported + def getPowerConsumptionCurrentUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["unit"]) + + @handleNotSupported + def getPowerConsumptionDHWThisYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.dhw")["properties"]["year"]["value"][0]) + + @handleNotSupported + def getPowerConsumptionHeatingThisYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.heating")["properties"]["year"]["value"][0]) + + @handleNotSupported + def getPowerConsumptionTotalThisYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0]) + + # Power consumption summary for DHW: + @handleNotSupported + def getPowerConsumptionSummaryDHWUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["unit"]) + + @handleNotSupported + def getPowerConsumptionSummaryDHWCurrentDay(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryDHWCurrentMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentMonth"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryDHWCurrentYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentYear"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryDHWLastMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastMonth"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryDHWLastSevenDays(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastSevenDays"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryDHWLastYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastYear"]["value"]) + + # Power consumption summary for Heating: + @handleNotSupported + def getPowerConsumptionSummaryHeatingUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["unit"]) + + @handleNotSupported + def getPowerConsumptionSummaryHeatingCurrentDay(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryHeatingCurrentMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentMonth"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryHeatingCurrentYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentYear"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryHeatingLastMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastMonth"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryHeatingLastSevenDays(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastSevenDays"]["value"]) + + @handleNotSupported + def getPowerConsumptionSummaryHeatingLastYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastYear"]["value"]) + + # Runtime by level + @handleNotSupported + def getRuntimeLevelOne(self) -> int: + return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["value"]) + + @handleNotSupported + def getRuntimeLevelTwo(self) -> int: + return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelTwo"]["value"]) + + @handleNotSupported + def getRuntimeLevelOneUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["unit"]) diff --git a/tests/test_Vitocal250A.py b/tests/test_Vitocal250A.py index 2573bb8a..2a4c3dcb 100644 --- a/tests/test_Vitocal250A.py +++ b/tests/test_Vitocal250A.py @@ -226,44 +226,40 @@ def test_getSeasonalPerformanceFactor(self): self.assertEqual(self.device.getSeasonalPerformanceFactorTotal(), 3.9) def test_getHeatingRod(self): - # self.assertEqual(self.device.getHeatingRodHeatProductionCurrent(), 0) # not in dump - # self.assertEqual(self.device.getHeatingRodPowerConsumptionCurrent(), 0) # not in dump - # self.assertEqual(self.device.getHeatingRodPowerConsumptionDHWThisYear(), 0) - # self.assertEqual(self.device.getHeatingRodPowerConsumptionHeatingThisYear(), 0) - self.assertEqual(self.device.getHeatingRodStarts(), 314) - self.assertEqual(self.device.getHeatingRodHours(), 31) + self.assertEqual(self.device.heatingRod.getStarts(), 314) + self.assertEqual(self.device.heatingRod.getHours(), 31) def test_getHeatingRodPowerConsumptionSummaryDHW(self): self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryDHWUnit(), "kilowattHour") + self.device.heatingRod.getPowerConsumptionSummaryDHWUnit(), "kilowattHour") self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentDay(), 0) + self.device.heatingRod.getPowerConsumptionSummaryDHWCurrentDay(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentMonth(), 0) + self.device.heatingRod.getPowerConsumptionSummaryDHWCurrentMonth(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentYear(), 3.3) + self.device.heatingRod.getPowerConsumptionSummaryDHWCurrentYear(), 3.3) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryDHWLastMonth(), 0) + self.device.heatingRod.getPowerConsumptionSummaryDHWLastMonth(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryDHWLastSevenDays(), 0) + self.device.heatingRod.getPowerConsumptionSummaryDHWLastSevenDays(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryDHWLastYear(), 28) + self.device.heatingRod.getPowerConsumptionSummaryDHWLastYear(), 28) def test_getHeatingRodPowerConsumptionSummaryHeating(self): self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryHeatingUnit(), "kilowattHour") + self.device.heatingRod.getPowerConsumptionSummaryHeatingUnit(), "kilowattHour") self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentDay(), 0) + self.device.heatingRod.getPowerConsumptionSummaryHeatingCurrentDay(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentMonth(), 0) + self.device.heatingRod.getPowerConsumptionSummaryHeatingCurrentMonth(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentYear(), 29.5) + self.device.heatingRod.getPowerConsumptionSummaryHeatingCurrentYear(), 29.5) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryHeatingLastMonth(), 0) + self.device.heatingRod.getPowerConsumptionSummaryHeatingLastMonth(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryHeatingLastSevenDays(), 0) + self.device.heatingRod.getPowerConsumptionSummaryHeatingLastSevenDays(), 0) self.assertEqual( - self.device.getHeatingRodPowerConsumptionSummaryHeatingLastYear(), 53.6) + self.device.heatingRod.getPowerConsumptionSummaryHeatingLastYear(), 53.6) def test_inverter_getCurrent(self): self.assertEqual(self.device.inverters[0].getCurrent(), 0) diff --git a/tests/test_Vitocal300G.py b/tests/test_Vitocal300G.py index e603e6a5..911d4d40 100644 --- a/tests/test_Vitocal300G.py +++ b/tests/test_Vitocal300G.py @@ -223,11 +223,11 @@ def test_getMainECURuntimeUnit(self): # Heating rod runtime tests def test_getHeatingRodRuntimeLevelOne(self): self.assertEqual( - self.device.getHeatingRodRuntimeLevelOne(), 886682) + self.device.heatingRod.getRuntimeLevelOne(), 886682) def test_getHeatingRodRuntimeLevelTwo(self): self.assertEqual( - self.device.getHeatingRodRuntimeLevelTwo(), 287877) + self.device.heatingRod.getRuntimeLevelTwo(), 287877) # Configuration tests def test_getConfigurationBufferTemperatureMax(self): From dc91457483a3131d0e085acf4d1138713134a04e Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Wed, 18 Feb 2026 09:36:16 +0100 Subject: [PATCH 4/5] 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 --- PyViCare/PyViCareHeatPump.py | 223 +++++++++++++++++------------------ tests/test_Vitocal250A.py | 32 ++--- tests/test_Vitocal300G.py | 4 +- 3 files changed, 124 insertions(+), 135 deletions(-) diff --git a/PyViCare/PyViCareHeatPump.py b/PyViCare/PyViCareHeatPump.py index 362926fb..6b85c37e 100644 --- a/PyViCare/PyViCareHeatPump.py +++ b/PyViCare/PyViCareHeatPump.py @@ -343,9 +343,112 @@ def getCoefficientOfPerformanceTotal(self) -> float: def getCoefficientOfPerformanceCooling(self) -> float: return float(self.getProperty("heating.cop.cooling")["properties"]["value"]["value"]) - @property - def heatingRod(self) -> HeatingRod: - return HeatingRod(self) + @handleNotSupported + def getHeatingRodStarts(self) -> int: + return int(self.getProperty("heating.heatingRod.statistics")["properties"]["starts"]["value"]) + + @handleNotSupported + def getHeatingRodHours(self) -> int: + return int(self.getProperty("heating.heatingRod.statistics")["properties"]["hours"]["value"]) + + @handleNotSupported + def getHeatingRodHeatProductionCurrent(self) -> float: + return float(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["value"]) + + @handleNotSupported + def getHeatingRodHeatProductionCurrentUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["unit"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionCurrent(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionCurrentUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["unit"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionDHWThisYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.dhw")["properties"]["year"]["value"][0]) + + @handleNotSupported + def getHeatingRodPowerConsumptionHeatingThisYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.heating")["properties"]["year"]["value"][0]) + + @handleNotSupported + def getHeatingRodPowerConsumptionTotalThisYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0]) + + # Heating rod power consumption summary for DHW: + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["unit"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWCurrentDay(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWCurrentMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWCurrentYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentYear"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWLastMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWLastSevenDays(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastSevenDays"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryDHWLastYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastYear"]["value"]) + + # Heating rod power consumption summary for Heating: + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["unit"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingCurrentDay(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingCurrentMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingCurrentYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentYear"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingLastMonth(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastMonth"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingLastSevenDays(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastSevenDays"]["value"]) + + @handleNotSupported + def getHeatingRodPowerConsumptionSummaryHeatingLastYear(self) -> float: + return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastYear"]["value"]) + + # Heating rod runtime by level + @handleNotSupported + def getHeatingRodRuntimeLevelOne(self) -> int: + return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["value"]) + + @handleNotSupported + def getHeatingRodRuntimeLevelTwo(self) -> int: + return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelTwo"]["value"]) + + @handleNotSupported + def getHeatingRodRuntimeLevelOneUnit(self) -> str: + return str(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["unit"]) # Additional pressure sensors (refrigerant circuit) @handleNotSupported @@ -809,117 +912,3 @@ def getTemperature(self) -> float: return float(self.getProperty(f"heating.inverters.{self.inverter}.sensors.temperature.powerModule")["properties"]["value"]["value"]) -class HeatingRod: - - def __init__(self, device: HeatPump) -> None: - self.service = device.service - - def getProperty(self, property_name: str) -> Any: - return self.service.getProperty(property_name) - - @handleNotSupported - def getStarts(self) -> int: - return int(self.getProperty("heating.heatingRod.statistics")["properties"]["starts"]["value"]) - - @handleNotSupported - def getHours(self) -> int: - return int(self.getProperty("heating.heatingRod.statistics")["properties"]["hours"]["value"]) - - @handleNotSupported - def getHeatProductionCurrent(self) -> float: - return float(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["value"]) - - @handleNotSupported - def getHeatProductionCurrentUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.heat.production.current")["properties"]["value"]["unit"]) - - @handleNotSupported - def getPowerConsumptionCurrent(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["value"]) - - @handleNotSupported - def getPowerConsumptionCurrentUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.power.consumption.current")["properties"]["value"]["unit"]) - - @handleNotSupported - def getPowerConsumptionDHWThisYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.dhw")["properties"]["year"]["value"][0]) - - @handleNotSupported - def getPowerConsumptionHeatingThisYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.heating")["properties"]["year"]["value"][0]) - - @handleNotSupported - def getPowerConsumptionTotalThisYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0]) - - # Power consumption summary for DHW: - @handleNotSupported - def getPowerConsumptionSummaryDHWUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["unit"]) - - @handleNotSupported - def getPowerConsumptionSummaryDHWCurrentDay(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentDay"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryDHWCurrentMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentMonth"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryDHWCurrentYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["currentYear"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryDHWLastMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastMonth"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryDHWLastSevenDays(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastSevenDays"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryDHWLastYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.dhw")["properties"]["lastYear"]["value"]) - - # Power consumption summary for Heating: - @handleNotSupported - def getPowerConsumptionSummaryHeatingUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["unit"]) - - @handleNotSupported - def getPowerConsumptionSummaryHeatingCurrentDay(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentDay"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryHeatingCurrentMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentMonth"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryHeatingCurrentYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["currentYear"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryHeatingLastMonth(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastMonth"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryHeatingLastSevenDays(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastSevenDays"]["value"]) - - @handleNotSupported - def getPowerConsumptionSummaryHeatingLastYear(self) -> float: - return float(self.getProperty("heating.heatingRod.power.consumption.summary.heating")["properties"]["lastYear"]["value"]) - - # Runtime by level - @handleNotSupported - def getRuntimeLevelOne(self) -> int: - return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["value"]) - - @handleNotSupported - def getRuntimeLevelTwo(self) -> int: - return int(self.getProperty("heating.heatingRod.runtime")["properties"]["levelTwo"]["value"]) - - @handleNotSupported - def getRuntimeLevelOneUnit(self) -> str: - return str(self.getProperty("heating.heatingRod.runtime")["properties"]["levelOne"]["unit"]) diff --git a/tests/test_Vitocal250A.py b/tests/test_Vitocal250A.py index 2a4c3dcb..5b051a6d 100644 --- a/tests/test_Vitocal250A.py +++ b/tests/test_Vitocal250A.py @@ -226,40 +226,40 @@ def test_getSeasonalPerformanceFactor(self): self.assertEqual(self.device.getSeasonalPerformanceFactorTotal(), 3.9) def test_getHeatingRod(self): - self.assertEqual(self.device.heatingRod.getStarts(), 314) - self.assertEqual(self.device.heatingRod.getHours(), 31) + self.assertEqual(self.device.getHeatingRodStarts(), 314) + self.assertEqual(self.device.getHeatingRodHours(), 31) def test_getHeatingRodPowerConsumptionSummaryDHW(self): self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryDHWUnit(), "kilowattHour") + self.device.getHeatingRodPowerConsumptionSummaryDHWUnit(), "kilowattHour") self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryDHWCurrentDay(), 0) + self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentDay(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryDHWCurrentMonth(), 0) + self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentMonth(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryDHWCurrentYear(), 3.3) + self.device.getHeatingRodPowerConsumptionSummaryDHWCurrentYear(), 3.3) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryDHWLastMonth(), 0) + self.device.getHeatingRodPowerConsumptionSummaryDHWLastMonth(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryDHWLastSevenDays(), 0) + self.device.getHeatingRodPowerConsumptionSummaryDHWLastSevenDays(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryDHWLastYear(), 28) + self.device.getHeatingRodPowerConsumptionSummaryDHWLastYear(), 28) def test_getHeatingRodPowerConsumptionSummaryHeating(self): self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryHeatingUnit(), "kilowattHour") + self.device.getHeatingRodPowerConsumptionSummaryHeatingUnit(), "kilowattHour") self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryHeatingCurrentDay(), 0) + self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentDay(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryHeatingCurrentMonth(), 0) + self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentMonth(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryHeatingCurrentYear(), 29.5) + self.device.getHeatingRodPowerConsumptionSummaryHeatingCurrentYear(), 29.5) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryHeatingLastMonth(), 0) + self.device.getHeatingRodPowerConsumptionSummaryHeatingLastMonth(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryHeatingLastSevenDays(), 0) + self.device.getHeatingRodPowerConsumptionSummaryHeatingLastSevenDays(), 0) self.assertEqual( - self.device.heatingRod.getPowerConsumptionSummaryHeatingLastYear(), 53.6) + self.device.getHeatingRodPowerConsumptionSummaryHeatingLastYear(), 53.6) def test_inverter_getCurrent(self): self.assertEqual(self.device.inverters[0].getCurrent(), 0) diff --git a/tests/test_Vitocal300G.py b/tests/test_Vitocal300G.py index 911d4d40..e603e6a5 100644 --- a/tests/test_Vitocal300G.py +++ b/tests/test_Vitocal300G.py @@ -223,11 +223,11 @@ def test_getMainECURuntimeUnit(self): # Heating rod runtime tests def test_getHeatingRodRuntimeLevelOne(self): self.assertEqual( - self.device.heatingRod.getRuntimeLevelOne(), 886682) + self.device.getHeatingRodRuntimeLevelOne(), 886682) def test_getHeatingRodRuntimeLevelTwo(self): self.assertEqual( - self.device.heatingRod.getRuntimeLevelTwo(), 287877) + self.device.getHeatingRodRuntimeLevelTwo(), 287877) # Configuration tests def test_getConfigurationBufferTemperatureMax(self): From 6713a0df0f8c87962691ccd2c196bcb08431b197 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:56:07 +0100 Subject: [PATCH 5/5] Remove unnecessary blank lines in getTemperature method --- PyViCare/PyViCareHeatPump.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/PyViCare/PyViCareHeatPump.py b/PyViCare/PyViCareHeatPump.py index 6b85c37e..74da5477 100644 --- a/PyViCare/PyViCareHeatPump.py +++ b/PyViCare/PyViCareHeatPump.py @@ -910,5 +910,3 @@ def getPower(self) -> float: @handleNotSupported def getTemperature(self) -> float: return float(self.getProperty(f"heating.inverters.{self.inverter}.sensors.temperature.powerModule")["properties"]["value"]["value"]) - -