Skip to content
Open
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
Binary file added .DS_Store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldnt be added

Binary file not shown.
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.13
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff
Expand Down
Binary file added custom_components/.DS_Store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt be here

Binary file not shown.
4 changes: 4 additions & 0 deletions custom_components/tech/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
websession = async_get_clientsession(hass)

coordinator = TechCoordinator(hass, websession, user_id, token)
coordinator.config_entry = entry
hass.data[DOMAIN][entry.entry_id] = coordinator

await coordinator.async_config_entry_first_refresh()

# Load filter reset date from storage
await coordinator.async_load_filter_reset_date()

await assets.load_subtitles(language_code, Tech(websession, user_id, token))

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
Expand Down
176 changes: 176 additions & 0 deletions custom_components/tech/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,38 @@
from homeassistant.components import binary_sensor
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
CONF_MODEL,
CONF_NAME,
CONF_PARAMS,
CONF_TYPE,
STATE_OFF,
STATE_ON,
EntityCategory,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType, UndefinedType
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import TechCoordinator, assets
from .const import (
CONTROLLER,
DOMAIN,
FILTER_ALARM_MAX_DAYS,
INCLUDE_HUB_IN_NAME,
MANUFACTURER,
RECUPERATION_EXHAUST_FLOW,
RECUPERATION_SUPPLY_FLOW,
RECUPERATION_SUPPLY_FLOW_ALT,
TYPE_ADDITIONAL_PUMP,
TYPE_FIRE_SENSOR,
TYPE_RELAY,
TYPE_TEMPERATURE_CH,
UDID,
VER,
VISIBILITY,
)
from .entity import TileEntity
Expand Down Expand Up @@ -63,6 +77,28 @@ async def async_setup_entry(
if tile[CONF_TYPE] == TYPE_ADDITIONAL_PUMP:
entities.append(RelaySensor(tile, coordinator, config_entry))

# Check if we have recuperation system for filter replacement sensor
has_recuperation_flow = False
for t in tiles:
tile = tiles[t]
if tile.get("type") == TYPE_TEMPERATURE_CH:
widget1_txt_id = tile.get("params", {}).get("widget1", {}).get("txtId", 0)
widget2_txt_id = tile.get("params", {}).get("widget2", {}).get("txtId", 0)
for flow_sensor in [RECUPERATION_EXHAUST_FLOW, RECUPERATION_SUPPLY_FLOW, RECUPERATION_SUPPLY_FLOW_ALT]:
if flow_sensor["txt_id"] in [widget1_txt_id, widget2_txt_id]:
has_recuperation_flow = True
break
if has_recuperation_flow:
break

# Add recuperation binary sensors if system detected
if has_recuperation_flow:
_LOGGER.debug("Creating recuperation binary sensors")
entities.extend([
FilterReplacementSensor(coordinator, config_entry),
RecuperationSystemStatusSensor(coordinator, config_entry),
])

async_add_entities(entities, True)


Expand Down Expand Up @@ -109,3 +145,143 @@ def __init__(
def get_state(self, device):
"""Get device state."""
return device[CONF_PARAMS]["workingStatus"]


class FilterReplacementSensor(CoordinatorEntity, binary_sensor.BinarySensorEntity):
"""Binary sensor to indicate when filter replacement is needed."""

_attr_has_entity_name = True
_attr_device_class = binary_sensor.BinarySensorDeviceClass.PROBLEM
_attr_icon = "mdi:air-filter-outline"

def __init__(
self,
coordinator: TechCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the filter replacement sensor."""
super().__init__(coordinator)
self._coordinator = coordinator
self._config_entry = config_entry
self._udid = config_entry.data[CONTROLLER][UDID]
self._attr_unique_id = f"{self._udid}_filter_replacement_needed"

self._name = (
self._config_entry.title + " "
if self._config_entry.data[INCLUDE_HUB_IN_NAME]
else ""
) + "Filter Replacement Needed"

@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name

@property
def is_on(self) -> bool | None:
"""Return True if filter replacement is needed."""
# Calculate if filter replacement is needed based on usage days
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is source of max days and other constants?

Does device record them or what will happen when those will be passed?

if hasattr(self._coordinator, '_filter_reset_date') and self._coordinator._filter_reset_date:
from datetime import datetime
reset_date = datetime.fromisoformat(self._coordinator._filter_reset_date)
current_date = datetime.now()
days_since_reset = (current_date - reset_date).days
return days_since_reset >= FILTER_ALARM_MAX_DAYS
else:
# No reset date stored, check if we're over the default max days
# Assume filter is new if no reset date
return False

@property
def entity_category(self):
"""Return the entity category for diagnostic entities."""
return EntityCategory.DIAGNOSTIC

@property
def device_info(self) -> DeviceInfo | None:
"""Returns device information in a dictionary format."""
return {
ATTR_IDENTIFIERS: {
(DOMAIN, f"{self._udid}_recuperation")
},
CONF_NAME: f"{self._config_entry.title} Recuperation",
CONF_MODEL: (
self._config_entry.data[CONTROLLER][CONF_NAME]
+ ": "
+ self._config_entry.data[CONTROLLER][VER]
),
ATTR_MANUFACTURER: MANUFACTURER,
}


class RecuperationSystemStatusSensor(CoordinatorEntity, binary_sensor.BinarySensorEntity):
"""Binary sensor to indicate recuperation system operational status."""

_attr_has_entity_name = True
_attr_device_class = binary_sensor.BinarySensorDeviceClass.RUNNING
_attr_icon = "mdi:air-filter"

def __init__(
self,
coordinator: TechCoordinator,
config_entry: ConfigEntry,
) -> None:
"""Initialize the system status sensor."""
super().__init__(coordinator)
self._coordinator = coordinator
self._config_entry = config_entry
self._udid = config_entry.data[CONTROLLER][UDID]
self._attr_unique_id = f"{self._udid}_recuperation_running"

self._name = (
self._config_entry.title + " "
if self._config_entry.data[INCLUDE_HUB_IN_NAME]
else ""
) + "Recuperation Running"

@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name

@property
def is_on(self) -> bool | None:
"""Return True if recuperation system is running."""
# Check if any flow sensors show activity
if self._coordinator.data and "tiles" in self._coordinator.data:
for tile_id, tile_data in self._coordinator.data["tiles"].items():
if tile_data.get("type") == TYPE_TEMPERATURE_CH:
# Check flow sensors for activity
widget1_data = tile_data.get("params", {}).get("widget1", {})
widget2_data = tile_data.get("params", {}).get("widget2", {})

# Check if any flow sensor shows activity (> 0)
for widget_data in [widget1_data, widget2_data]:
widget_txt_id = widget_data.get("txtId", 0)
for flow_sensor in [RECUPERATION_EXHAUST_FLOW, RECUPERATION_SUPPLY_FLOW, RECUPERATION_SUPPLY_FLOW_ALT]:
if flow_sensor["txt_id"] == widget_txt_id:
flow_value = widget_data.get("value", 0)
if flow_value and flow_value > 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we dont need to check flow_value exists as above you always make it using:
flow_value = widget_data.get("value", 0)

return True
return False

@property
def entity_category(self):
"""Return the entity category for diagnostic entities."""
return EntityCategory.DIAGNOSTIC

@property
def device_info(self) -> DeviceInfo | None:
"""Returns device information in a dictionary format."""
return {
ATTR_IDENTIFIERS: {
(DOMAIN, f"{self._udid}_recuperation")
},
CONF_NAME: f"{self._config_entry.title} Recuperation",
CONF_MODEL: (
self._config_entry.data[CONTROLLER][CONF_NAME]
+ ": "
+ self._config_entry.data[CONTROLLER][VER]
),
ATTR_MANUFACTURER: MANUFACTURER,
}
Loading