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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 212 additions & 15 deletions PyViCare/PyViCareHeatPump.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations
from contextlib import suppress
from typing import Any, List
from deprecated import deprecated

from PyViCare.PyViCareHeatingDevice import HeatingDevice, HeatingDeviceWithComponent
from PyViCare.PyViCareUtils import handleAPICommandErrors, handleNotSupported
from PyViCare.PyViCareUtils import (PyViCareNotSupportedFeatureError,
handleAPICommandErrors, handleNotSupported)
from PyViCare.PyViCareVentilationDevice import VentilationDevice


Expand Down Expand Up @@ -41,13 +43,21 @@ def inverters(self) -> List[Inverter]:
def getInverter(self, inverter) -> Inverter:
return Inverter(self, inverter)

@handleNotSupported
def getBufferMainTemperature(self):
return self.getProperty("heating.bufferCylinder.sensors.temperature.main")["properties"]['value']['value']
"""Get buffer tank main temperature. Tries 'buffer' path first, then 'bufferCylinder'."""
with suppress(KeyError):
return self.getProperty("heating.buffer.sensors.temperature.main")["properties"]['value']['value']
with suppress(KeyError):
return self.getProperty("heating.bufferCylinder.sensors.temperature.main")["properties"]['value']['value']
raise PyViCareNotSupportedFeatureError("getBufferMainTemperature")

@handleNotSupported
def getBufferTopTemperature(self):
return self.getProperty("heating.bufferCylinder.sensors.temperature.top")["properties"]['value']['value']
"""Get buffer tank top temperature. Tries 'buffer' path first, then 'bufferCylinder'."""
with suppress(KeyError):
return self.getProperty("heating.buffer.sensors.temperature.top")["properties"]['value']['value']
with suppress(KeyError):
return self.getProperty("heating.bufferCylinder.sensors.temperature.top")["properties"]['value']['value']
raise PyViCareNotSupportedFeatureError("getBufferTopTemperature")

# Power consumption for Heating:
@handleNotSupported
Expand Down Expand Up @@ -284,6 +294,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"])
Expand Down Expand Up @@ -320,6 +352,133 @@ def getHeatingRodPowerConsumptionHeatingThisYear(self) -> float:
def getHeatingRodPowerConsumptionTotalThisYear(self) -> float:
return float(self.getProperty("heating.heatingRod.power.consumption.total")["properties"]["year"]["value"][0])

# 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]:
return [self.getCoolingCircuit(x) for x in self.getAvailableCoolingCircuits()]

def getCoolingCircuit(self, circuit) -> CoolingCircuit:
return CoolingCircuit(self, circuit)

def getAvailableCoolingCircuits(self):
"""Detect available cooling circuits (0, 1, 2, etc.)."""
available = []
for circuit in ['0', '1', '2', '3']:
with suppress(KeyError, PyViCareNotSupportedFeatureError):
# Check for type feature as indicator that circuit exists
if self.getProperty(f"heating.coolingCircuits.{circuit}.type") is not None:
available.append(circuit)
return available


class CoolingCircuit(HeatingDeviceWithComponent):
"""Cooling circuit component for heat pumps with cooling capability."""

@property
def circuit(self) -> str:
return self.component

@handleNotSupported
def getType(self) -> str:
return str(self.getProperty(f"heating.coolingCircuits.{self.circuit}.type")["properties"]["value"]["value"])

@handleNotSupported
def getReverseActive(self) -> bool:
return bool(self.getProperty(f"heating.coolingCircuits.{self.circuit}.reverse")["properties"]["active"]["value"])


class Compressor(HeatingDeviceWithComponent):

Expand All @@ -335,25 +494,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):
Expand All @@ -363,6 +542,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"])
Expand Down
33 changes: 23 additions & 10 deletions PyViCare/PyViCareHeatingDevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,23 @@ def getDomesticHotWaterConfiguredTemperature(self):

@handleNotSupported
def getDomesticHotWaterStorageTemperature(self):
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder")["properties"]["value"][
"value"]
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder")["properties"]["value"]["value"]

@handleNotSupported
def getHotWaterStorageTemperatureTop(self):
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.top")["properties"]["value"][
"value"]
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.top")["properties"]["value"]["value"]

@handleNotSupported
def getDomesticHotWaterStorageTemperatureMiddle(self):
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.middle")["properties"]["value"][
"value"]
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.middle")["properties"]["value"]["value"]

@handleNotSupported
def getDomesticHotWaterStorageTemperatureMidBottom(self):
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.midBottom")["properties"]["value"][
"value"]
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.midBottom")["properties"]["value"]["value"]

@handleNotSupported
def getHotWaterStorageTemperatureBottom(self):
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.bottom")["properties"]["value"][
"value"]
return self.getProperty("heating.dhw.sensors.temperature.dhwCylinder.bottom")["properties"]["value"]["value"]

@handleNotSupported
def getDomesticHotWaterConfiguredTemperature2(self):
Expand Down Expand Up @@ -358,6 +353,15 @@ def getReturnTemperaturePrimaryCircuit(self):
return self.getProperty("heating.primaryCircuit.sensors.temperature.return")["properties"]["value"][
"value"]

@handleNotSupported
def getPrimaryCircuitPumpRotation(self):
"""Get primary circuit pump rotation/speed as percentage."""
return 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"][
Expand Down Expand Up @@ -538,6 +542,15 @@ def getSupplyTemperature(self):
self.getProperty(f"heating.circuits.{self.circuit}.sensors.temperature.supply")["properties"][
"value"]["value"]

@handleNotSupported
def getTemperature(self):
"""Get the circuit flow temperature setpoint."""
return self.getProperty(f"heating.circuits.{self.circuit}.temperature")["properties"]["value"]["value"]

@handleNotSupported
def getTemperatureUnit(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"][
Expand Down
9 changes: 9 additions & 0 deletions tests/test_TestForMissingProperties.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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()
Expand Down Expand Up @@ -361,6 +369,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)

Expand Down
Loading
Loading