Skip to content

Commit

Permalink
Filter change and code cleanup
Browse files Browse the repository at this point in the history
- Temperature filtering changed to Sensor filtering.
- Humidity, Air Quality and Room temperature is added to the filter.
- The Temperature Filtering Threshold entity is removed.
- The filter is changed to not only use the sliding average in the initialization fase.
- Some code cleanup.
  • Loading branch information
Tvalley71 committed Mar 1, 2025
1 parent 485a38e commit d23e9ee
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 178 deletions.
16 changes: 2 additions & 14 deletions custom_components/dantherm/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,8 @@ def native_value(self):
async def async_update(self) -> None:
"""Update the state of the cover."""

if self.entity_description.data_getinternal:
if hasattr(
self._device, f"async_{self.entity_description.data_getinternal}"
):
func = getattr(
self._device, f"async_{self.entity_description.data_getinternal}"
)
result = await func()
else:
result = getattr(self._device, self.entity_description.data_getinternal)
else:
result = await self._device.read_holding_registers(
description=self.entity_description
)
# Get the entity state
result = await self._device.async_get_entity_state(self.entity_description)

if result is None:
self._attr_available = False
Expand Down
147 changes: 95 additions & 52 deletions custom_components/dantherm/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from .const import DEFAULT_NAME, DEVICE_TYPES, DOMAIN
from .device_map import (
ATTR_SENSOR_FILTERING,
ATTR_TEMPERATURE_FILTERING_THRESHOLD,
STATE_AUTOMATIC,
STATE_AWAY,
STATE_FIREPLACE,
Expand All @@ -27,6 +26,7 @@
BypassDamperState,
ComponentClass,
CurrentUnitMode,
DanthermEntityDescription,
DataClass,
HacComponentClass,
)
Expand Down Expand Up @@ -137,34 +137,34 @@ def __init__(
self._entities = []
self.data = {}

# Dictionary to store history and last valid value for each temperature
# Initialize filtered sensors
self._filtered_sensors = {
"outdoor": {
"humidity": {
"history": deque(maxlen=5),
"max_change": 2,
"max_change": 5,
"initialized": False,
},
"supply": {
"air_quality": {
"history": deque(maxlen=5),
"max_change": 2,
"max_change": 50,
"initialized": False,
},
"extract": {
"exhaust": {
"history": deque(maxlen=5),
"max_change": 2,
"initialized": False,
},
"exhaust": {
"extract": {
"history": deque(maxlen=5),
"max_change": 2,
"initialized": False,
},
"humidity": {
"supply": {
"history": deque(maxlen=5),
"max_change": 2,
"initialized": False,
},
"voc": {
"outdoor": {
"history": deque(maxlen=5),
"max_change": 2,
"initialized": False,
Expand Down Expand Up @@ -265,7 +265,7 @@ async def async_install_entity(self, description: EntityDescription) -> bool:
or description.data_exclude_if_below
or description.data_exclude_if is not None
):
result = await self.read_holding_registers(description=description)
result = await self.async_get_entity_state(description)

if (
(
Expand Down Expand Up @@ -306,7 +306,9 @@ async def async_remove_refresh_entity(self, entity):
_LOGGER.debug("Removing refresh entity=%s", entity.name)
self._entities.remove(entity)

self.data.pop(entity.key, None)
# Remove the entity from the data dictionary
if entity.key in self.data:
self.data.pop(entity.key)

if not self._entities:
# This is the last entity, stop the interval timer
Expand Down Expand Up @@ -774,54 +776,33 @@ async def set_manual_bypass_duration(self, value):
await self._write_holding_uint32(264, value)

@property
def get_temperature_filtering(self):
"""Get temperature filtering."""
def get_sensor_filtering(self):
"""Get sensor filtering."""

return self.data.get(ATTR_SENSOR_FILTERING, False)

async def set_temperature_filtering(self, value):
async def set_sensor_filtering(self, value):
"""Set temperature filtering."""

self.data[ATTR_SENSOR_FILTERING] = value

@property
def get_temperature_filtering_threshold(self):
"""Get temperature filtering threshold."""

return self.data.get(ATTR_TEMPERATURE_FILTERING_THRESHOLD, self._max_change)

async def set_temperature_filtering_threshold(self, value):
"""Set temperature filtering threshold."""
async def async_get_humidity(self):
"""Get humidity."""

self.data[ATTR_TEMPERATURE_FILTERING_THRESHOLD] = value

def filter_temperature(self, sensor: str, new_value: float) -> float:
"""Filter the temperature for a given sensor, ensuring smooth initialization and spike reduction."""

# Ensure the sensor type is valid
if sensor not in self._filtered_sensors:
raise ValueError(f"Invalid sensor: {sensor}")
new_value = await self._read_holding_uint32(address=196)

sensor_data = self._filtered_sensors[sensor]
history: deque = sensor_data["history"]

# Collect initial samples and compute average until initialized
history.append(new_value)
if not sensor_data["initialized"]:
if len(history) < history.maxlen:
return sum(history) / len(history)
sensor_data["initialized"] = True
if not self._sensor_filtering:
return new_value
return self._filter_sensor("humidity", new_value)

# Compute rolling average
rolling_average = sum(history) / len(history)
async def async_get_air_quality(self):
"""Get air quality."""

# If new value is a spike, return rolling average (reject spike)
if abs(new_value - rolling_average) > sensor_data["max_change"]:
return round(rolling_average, 1)
new_value = await self._read_holding_uint32(address=430)

# Otherwise, accept new value and update history
history.append(new_value)
return new_value
if not self._sensor_filtering:
return new_value
return self._filter_sensor("air_quality", new_value)

async def async_get_exhaust_temperature(self):
"""Get exhaust temperature."""
Expand All @@ -830,7 +811,7 @@ async def async_get_exhaust_temperature(self):

if not self._sensor_filtering:
return new_value
return self.filter_temperature("exhaust", new_value)
return self._filter_sensor("exhaust", new_value)

async def async_get_extract_temperature(self):
"""Get extract temperature."""
Expand All @@ -839,7 +820,7 @@ async def async_get_extract_temperature(self):

if not self._sensor_filtering:
return new_value
return self.filter_temperature("extract", new_value)
return self._filter_sensor("extract", new_value)

async def async_get_supply_temperature(self):
"""Get supply temperature."""
Expand All @@ -848,7 +829,7 @@ async def async_get_supply_temperature(self):

if not self._sensor_filtering:
return new_value
return self.filter_temperature("supply", new_value)
return self._filter_sensor("supply", new_value)

async def async_get_outdoor_temperature(self):
"""Get outdoor temperature."""
Expand All @@ -857,7 +838,69 @@ async def async_get_outdoor_temperature(self):

if not self._sensor_filtering:
return new_value
return self.filter_temperature("outdoor", new_value)
return self._filter_sensor("outdoor", new_value)

async def async_get_room_temperature(self):
"""Get room temperature."""

new_value = await self._read_holding_float32(address=140, precision=1)

if not self._sensor_filtering:
return new_value
return self._filter_sensor("room", new_value)

def _filter_sensor(self, sensor: str, new_value: float) -> float:
"""Filter a given sensor, ensuring smooth initialization and spike reduction."""

# Ensure the sensor type is valid
if sensor not in self._filtered_sensors:
raise ValueError(f"Invalid sensor: {sensor}")

sensor_data = self._filtered_sensors[sensor]
history: deque = sensor_data["history"]

# Collect initial samples and compute average until initialized
history.append(new_value)
if not sensor_data["initialized"]:
if len(history) < history.maxlen:
return round(sum(history) / len(history), 1)
sensor_data["initialized"] = True

# Compute rolling average
rolling_average = sum(history) / len(history)

# If new value is a spike, return rolling average (reject spike)
if abs(new_value - rolling_average) > sensor_data["max_change"]:
return round(rolling_average, 1)

# Otherwise, accept new value and update history
history.append(new_value)
return new_value

async def async_get_entity_state(self, description: DanthermEntityDescription):
"""Get entity value from description."""

if description.data_getinternal:
if hasattr(self, f"async_{description.data_getinternal}"):
result = await getattr(self, f"async_{description.data_getinternal}")()
else:
result = getattr(self, description.data_getinternal)
elif description.data_entity:
result = self.data.get(description.data_entity, None)
else:
result = await self.read_holding_registers(description=description)

return result

async def async_get_entity_attrs(self, description: DanthermEntityDescription):
"""Get entity attributes from description."""

result = None
if hasattr(self, f"async_get_{description.key}_attrs"):
result = await getattr(self, f"async_get_{description.key}_attrs")()
elif hasattr(self, f"get_{description.key}_attrs"):
result = getattr(self, f"get_{description.key}_attrs")
return result

async def read_holding_registers(
self,
Expand Down
Loading

0 comments on commit d23e9ee

Please sign in to comment.