Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Added "Boiler heatpower" and "Total system power" sensors #138

Merged
merged 8 commits into from
Dec 12, 2024
42 changes: 42 additions & 0 deletions custom_components/quatt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,45 @@
DEFAULT_SCAN_INTERVAL: Final = 10
MIN_SCAN_INTERVAL: Final = 5
MAX_SCAN_INTERVAL: Final = 600

# Temperature-dependent conversion factors for water in a central heating system at 2 bar pressure.
# The table below provides specific heat capacity (c_p), density (rho), and conversion factors (k)
# for temperatures ranging from 5°C to 80°C in steps of 5°C.

# Temperature (C) | c_p (J/kg.C) | rho (kg/m^3) | Conversion Factor (k)
# -------------------------------------------------------------------------
# 5 | 4200.0 | 999.97 | 1.166667
# 10 | 4192.0 | 999.70 | 1.164444
# 15 | 4187.6 | 999.10 | 1.162889
# 20 | 4184.1 | 998.00 | 1.161111
# 25 | 4181.8 | 997.05 | 1.157438
# 30 | 4184.0 | 995.67 | 1.157753
# 35 | 4186.2 | 994.06 | 1.157931
# 40 | 4188.4 | 992.22 | 1.157964
# 45 | 4190.6 | 990.25 | 1.157859
# 50 | 4192.8 | 988.05 | 1.157617
# 55 | 4195.0 | 985.65 | 1.157243
# 60 | 4197.2 | 983.15 | 1.156742
# 65 | 4199.4 | 980.44 | 1.156117
# 70 | 4201.6 | 977.63 | 1.155369
# 75 | 4203.8 | 974.71 | 1.154503
# 80 | 4206.0 | 971.80 | 1.153528
# The specific heat capacity (c_p) and density (ρ) values are from the NIST Chemistry WebBook.
CONVERSION_FACTORS = {
5: 1.166667,
10: 1.164444,
15: 1.162889,
20: 1.161111,
25: 1.157438,
30: 1.157753,
35: 1.157931,
40: 1.157964,
45: 1.157859,
50: 1.157617,
55: 1.157243,
60: 1.156742,
65: 1.156117,
70: 1.155369,
75: 1.154503,
80: 1.153528,
}
137 changes: 110 additions & 27 deletions custom_components/quatt/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .api import QuattApiClient, QuattApiClientAuthenticationError, QuattApiClientError
from .const import CONF_POWER_SENSOR, DOMAIN, LOGGER
from .const import CONF_POWER_SENSOR, CONVERSION_FACTORS, DOMAIN, LOGGER


# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
Expand Down Expand Up @@ -67,6 +67,13 @@ def boilerOpenTherm(self):
LOGGER.debug(self.getValue("boiler.otFbChModeActive"))
return self.getValue("boiler.otFbChModeActive") is not None

def getConversionFactor(self, temperature: float):
"""Get the conversion factor for the nearest temperature."""
nearestTemperature = min(
CONVERSION_FACTORS.keys(), key=lambda t: abs(t - temperature)
)
return CONVERSION_FACTORS[nearestTemperature]

def electricalPower(self):
"""Get heatpump power from sensor."""
if self._power_sensor_id is None:
Expand Down Expand Up @@ -109,42 +116,118 @@ def computedWaterDelta(self, parent_key: str | None = None):

def computedHeatPower(self, parent_key: str | None = None):
"""Compute heatPower."""
computedWaterDelta = (
self.computedWaterDelta(None)
if self.heatpump2Active()
else self.computedWaterDelta("hp1")
)

# Retrieve the supervisory control mode state first
state = self.getValue("qc.supervisoryControlMode")
LOGGER.debug("computedBoilerHeatPower.supervisoryControlMode: %s", state)

# If the state is not valid or the heatpump is not active, no need to proceed
if state is None:
return None
if state not in [2, 3]:
return 0.0

if self.heatpump2Active():
computedWaterDelta = self.computedWaterDelta(None)
temperatureWaterOut = self.getValue("hp2.temperatureWaterOut")
else:
computedWaterDelta = self.computedWaterDelta("hp1")
temperatureWaterOut = self.getValue("hp1.temperatureWaterOut")
flowRate = self.getValue("qc.flowRateFiltered")

LOGGER.debug("computedHeatPower.computedWaterDelta %s", computedWaterDelta)
LOGGER.debug("computedHeatPower.flowRate %s", flowRate)
LOGGER.debug("computedHeatPower.temperatureWaterOut %s", temperatureWaterOut)

if computedWaterDelta is None or flowRate is None:
if (
computedWaterDelta is None
or flowRate is None
or temperatureWaterOut is None
):
return None

value = round(
computedWaterDelta * flowRate * 1.137888,
computedWaterDelta
* flowRate
* self.getConversionFactor(temperatureWaterOut),
2,
)

# Prevent negative sign for 0 values (like: -0.0)
if value == 0:
return math.copysign(0.0, 1)
return value
# Prevent any negative numbers
return max(value, 0.00)

def computedPowerInput(self, parent_key: str | None = None):
"""Compute total powerInput."""
powerInputHp1 = self.getValue("hp1.powerInput", 0)
powerInputHp2 = self.getValue("hp2.powerInput", 0)
def computedBoilerHeatPower(self, parent_key: str | None = None) -> float | None:
"""Compute the boiler's added heat power."""

return float(powerInputHp1) + float(powerInputHp2)
# Retrieve the supervisory control mode state first
state = self.getValue("qc.supervisoryControlMode")
LOGGER.debug("computedBoilerHeatPower.supervisoryControlMode: %s", state)

def computedPower(self, parent_key: str | None = None):
# If the state is not valid or the boiler is not active, no need to proceed
if state is None:
return None
if state not in [3, 4]:
return 0.0

# Retrieve other required values
heatpumpWaterOut = (
self.getValue("hp2.temperatureWaterOut")
if self.heatpump2Active()
else self.getValue("hp1.temperatureWaterOut")
)
flowRate = self.getValue("qc.flowRateFiltered")
flowWaterTemperature = self.getValue("flowMeter.waterSupplyTemperature")

# Log debug information
LOGGER.debug(
"computedBoilerHeatPower.temperatureWaterOut: %s", heatpumpWaterOut
)
LOGGER.debug("computedBoilerHeatPower.flowRate: %s", flowRate)
LOGGER.debug(
"computedBoilerHeatPower.waterSupplyTemperature: %s", flowWaterTemperature
)

# Validate other inputs
if heatpumpWaterOut is None or flowRate is None or flowWaterTemperature is None:
return None

# Compute the heat power using the conversion factor
conversionFactor = self.getConversionFactor(flowWaterTemperature)
value = round(
(flowWaterTemperature - heatpumpWaterOut) * flowRate * conversionFactor, 2
)

# Prevent any negative numbers
return max(value, 0.00)

def computedSystemPower(self, parent_key: str | None = None):
"""Compute total system power."""
boilerPower = self.computedBoilerHeatPower(parent_key)
heatpumpPower = self.computedPower(parent_key)

# Log debug information
LOGGER.debug("computedSystemPower.boilerPower: %s", boilerPower)
LOGGER.debug("computedSystemPower.heatpumpPower: %s", heatpumpPower)

# Validate inputs
if boilerPower is None or heatpumpPower is None:
return None

return float(boilerPower) + float(heatpumpPower)

def computedPowerInput(self, parent_key: str | None = None):
"""Compute total powerInput."""
powerHp1 = self.getValue("hp1.power", 0)
powerHp2 = self.getValue("hp2.power", 0)
powerInputHp1 = float(self.getValue("hp1.powerInput", 0))
powerInputHp2 = (
float(self.getValue("hp2.powerInput", 0)) if self.heatpump2Active() else 0
)
return powerInputHp1 + powerInputHp2

return float(powerHp1) + float(powerHp2)
def computedPower(self, parent_key: str | None = None):
"""Compute total power."""
powerHp1 = float(self.getValue("hp1.power", 0))
powerHp2 = float(self.getValue("hp2.power", 0)) if self.heatpump2Active() else 0
return powerHp1 + powerHp2

def computedCop(self, parent_key: str | None = None):
"""Compute COP."""
Expand All @@ -167,11 +250,8 @@ def computedCop(self, parent_key: str | None = None):
def computedQuattCop(self, parent_key: str | None = None):
"""Compute Quatt COP."""
if parent_key is None:
parent_key = ""
powerInput = self.getValue("hp1.powerInput", 0) + self.getValue(
"hp2.powerInput", 0
)
powerOutput = self.getValue("hp1.power", 0) + self.getValue("hp2.power", 0)
powerInput = self.computedPowerInput(parent_key)
powerOutput = self.computedPower(parent_key)
else:
powerInput = self.getValue(parent_key + ".powerInput")
powerOutput = self.getValue(parent_key + ".power")
Expand All @@ -187,7 +267,10 @@ def computedQuattCop(self, parent_key: str | None = None):
if powerInput == 0:
return None

return round(powerOutput / powerInput, 2)
value = round(powerOutput / powerInput, 2)

# Prevent negative sign for 0 values (like: -0.0)
return math.copysign(0.0, 1) if value == 0 else value

def computedDefrost(self, parent_key: str | None = None):
"""Compute Quatt Defrost State."""
Expand Down
22 changes: 18 additions & 4 deletions custom_components/quatt/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@
icon="mdi:lightning-bolt",
native_unit_of_measurement="W",
device_class=SensorDeviceClass.POWER,
entity_registry_enabled_default=False,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
quatt_duo=True,
Expand All @@ -204,18 +203,25 @@
icon="mdi:heat-wave",
native_unit_of_measurement="W",
device_class=SensorDeviceClass.POWER,
entity_registry_enabled_default=False,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
quatt_duo=True,
),
QuattSensorEntityDescription(
name="Total system power",
key="computedSystemPower",
icon="mdi:heat-wave",
native_unit_of_measurement="W",
device_class=SensorDeviceClass.POWER,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
QuattSensorEntityDescription(
name="Total water delta",
key="computedWaterDelta",
icon="mdi:thermometer-water",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
quatt_duo=True,
Expand All @@ -225,7 +231,6 @@
key="computedQuattCop",
icon="mdi:heat-pump",
native_unit_of_measurement="CoP",
entity_registry_enabled_default=False,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
quatt_duo=True,
Expand All @@ -251,6 +256,15 @@
state_class=SensorStateClass.MEASUREMENT,
quatt_opentherm=True,
),
QuattSensorEntityDescription(
name="Boiler heat power",
key="boiler.computedBoilerHeatPower",
icon="mdi:heat-wave",
native_unit_of_measurement="W",
device_class=SensorDeviceClass.POWER,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
# Flowmeter
QuattSensorEntityDescription(
name="Flowmeter temperature",
Expand Down
Loading