From 30626c535d6080c228976cbe729c401b23bafad7 Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Wed, 26 Feb 2025 12:06:05 +0100 Subject: [PATCH 01/11] Update README.md - Added Brink Flair 200 as supported device --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bdef132..24cc186 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ You have two options for installation: - Restart HA server. ### WORKING ON: +- Brink Flair 200 - Brink Renovent 180 - Brink Renovent 300 - Brink Renovent 400 Plus From a585ab7f2681894438d15bd379aafbafd893ffc5 Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Wed, 26 Feb 2025 12:52:03 +0100 Subject: [PATCH 02/11] Added support for CO2 sensors --- .../brink_ventilation/__init__.py | 9 +- .../core/brink_home_cloud.py | 48 +++++- custom_components/brink_ventilation/sensor.py | 163 ++++++++++++++++++ 3 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 custom_components/brink_ventilation/sensor.py diff --git a/custom_components/brink_ventilation/__init__.py b/custom_components/brink_ventilation/__init__.py index 9679443..bcc1dde 100644 --- a/custom_components/brink_ventilation/__init__.py +++ b/custom_components/brink_ventilation/__init__.py @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SELECT, Platform.BINARY_SENSOR, Platform.FAN] +PLATFORMS = [Platform.SELECT, Platform.BINARY_SENSOR, Platform.FAN, Platform.SENSOR] CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -100,9 +100,16 @@ async def async_get_devices(hass: HomeAssistant, entry: ConfigEntry, brink_clien # Retrieve additional description for system in systems: description = await brink_client.get_description_values(system["system_id"], system["gateway_id"]) + + # Add core ventilation control values system["ventilation"] = description["ventilation"] system["mode"] = description["mode"] system["filters_need_change"] = description["filters_need_change"] + + # Add any additional sensors (CO2, temperature, humidity, etc.) + for key, value in description.items(): + if key not in ["ventilation", "mode", "filters_need_change"]: + system[key] = value hass.data[DOMAIN][entry.entry_id][DATA_DEVICES] = systems diff --git a/custom_components/brink_ventilation/core/brink_home_cloud.py b/custom_components/brink_ventilation/core/brink_home_cloud.py index 5ddadca..4ff096f 100644 --- a/custom_components/brink_ventilation/core/brink_home_cloud.py +++ b/custom_components/brink_ventilation/core/brink_home_cloud.py @@ -113,20 +113,54 @@ async def get_description_values(self, system_id, gateway_id): ) menu_items = result.get("menuItems", []) + if not menu_items: + _LOGGER.debug("No menu items found in API response") + return {} + menu_item = menu_items[0] pages = menu_item.get("pages", []) - home_page = pages[0] - parameters = home_page.get("parameterDescriptors", []) - ventilation = self.__find(parameters, "uiId", "Lüftungsstufe") - mode = self.__find(parameters, "uiId", "Betriebsart") - filters_need_change = self.__find(parameters, "uiId", "Status Filtermeldung") - - + if not pages: + _LOGGER.debug("No pages found in menu item") + return {} + + # Extract all parameters from all pages + all_parameters = [] + for page in pages: + parameters = page.get("parameterDescriptors", []) + all_parameters.extend(parameters) + + _LOGGER.debug(f"Found {len(all_parameters)} parameters across all pages") + + # Find the basic parameters + ventilation = self.__find(all_parameters, "uiId", "Lüftungsstufe") + mode = self.__find(all_parameters, "uiId", "Betriebsart") + filters_need_change = self.__find(all_parameters, "uiId", "Status Filtermeldung") + + # Initialize the result dictionary with the basic parameters description_result = { "ventilation": self.__get_type(ventilation), "mode": self.__get_type(mode), "filters_need_change": self.__get_type(filters_need_change) } + + # Look for CO2 sensors and other sensors and add them to the result + for param in all_parameters: + param_name = param.get("name", "") + + # Add CO2 sensors + if "PPM eBus CO2-sensor" in param_name or "PPM CO2-sensor" in param_name: + _LOGGER.debug(f"Found CO2 sensor: {param_name}") + description_result[param_name] = self.__get_type(param) + + # Add temperature sensors + elif "temperatur" in param_name.lower(): + _LOGGER.debug(f"Found temperature sensor: {param_name}") + description_result[param_name] = self.__get_type(param) + + # Add humidity sensors + elif "feuchte" in param_name.lower(): + _LOGGER.debug(f"Found humidity sensor: {param_name}") + description_result[param_name] = self.__get_type(param) _LOGGER.debug( "get_description_values result: %s", diff --git a/custom_components/brink_ventilation/sensor.py b/custom_components/brink_ventilation/sensor.py new file mode 100644 index 0000000..3f0c126 --- /dev/null +++ b/custom_components/brink_ventilation/sensor.py @@ -0,0 +1,163 @@ +"""Support for Brink ventilation sensors.""" +from __future__ import annotations + +import logging +from typing import Optional + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION, UnitOfTemperature +from homeassistant.core import HomeAssistant + +from custom_components.brink_ventilation import BrinkHomeDeviceEntity + +from .const import ( + DATA_CLIENT, + DATA_COORDINATOR, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +# Define sensor types with their properties +SENSOR_TYPES = { + "co2": { + "device_class": SensorDeviceClass.CO2, + "state_class": SensorStateClass.MEASUREMENT, + "unit": CONCENTRATION_PARTS_PER_MILLION, + "icon": "mdi:molecule-co2", + "patterns": ["PPM eBus CO2-sensor", "PPM CO2-sensor", "CO2-sensor"], + }, + "temperature": { + "device_class": SensorDeviceClass.TEMPERATURE, + "state_class": SensorStateClass.MEASUREMENT, + "unit": UnitOfTemperature.CELSIUS, + "icon": "mdi:thermometer", + "patterns": ["temperatur", "Frischlufttemperatur", "Zulufttemperatur"], + }, + "humidity": { + "device_class": SensorDeviceClass.HUMIDITY, + "state_class": SensorStateClass.MEASUREMENT, + "unit": "%", + "icon": "mdi:water-percent", + "patterns": ["Feuchte", "humidity"], + }, +} + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): + """Set up the Brink Home sensor platform.""" + client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + entities = [] + + _LOGGER.debug(f"Setting up Brink ventilation sensors") + + # Look for CO2 sensors and other specific sensors in the data + for device_index, device_data in enumerate(coordinator.data): + _LOGGER.debug(f"Device {device_index} available keys: {list(device_data.keys())}") + + # Check for CO2 sensors and other sensors that might be stored as top-level keys + for key, value in device_data.items(): + if isinstance(value, dict) and "name" in value and "value" in value: + # Check if the key matches any of our sensor patterns + for sensor_type, properties in SENSOR_TYPES.items(): + for pattern in properties["patterns"]: + if pattern.lower() in key.lower(): + _LOGGER.debug(f"Found {sensor_type} sensor by key: {key}") + entities.append( + BrinkSensor( + client, + coordinator, + device_index, + key, + value["name"], + properties["device_class"], + properties["state_class"], + properties["unit"], + properties["icon"] + ) + ) + + # Also check the name field in the value dict as before + sensor_name = value.get("name", "") + _LOGGER.debug(f"Checking potential sensor: {sensor_name}") + + for sensor_type, properties in SENSOR_TYPES.items(): + for pattern in properties["patterns"]: + if pattern.lower() in sensor_name.lower(): + _LOGGER.debug(f"Found {sensor_type} sensor by name: {sensor_name}") + entities.append( + BrinkSensor( + client, + coordinator, + device_index, + key, + sensor_name, + properties["device_class"], + properties["state_class"], + properties["unit"], + properties["icon"] + ) + ) + + if entities: + _LOGGER.info(f"Adding {len(entities)} sensor entities: {[e.entity_name for e in entities]}") + async_add_entities(entities) + else: + _LOGGER.warning("No sensors found in the data. Make sure your Brink system has sensors and they are enabled.") + + +class BrinkSensor(BrinkHomeDeviceEntity, SensorEntity): + """Representation of a Brink sensor.""" + + def __init__(self, client, coordinator, device_index, entity_name, display_name, + device_class, state_class, unit, icon): + """Initialize the Brink sensor.""" + super().__init__(client, coordinator, device_index, entity_name) + self._display_name = display_name + self._attr_device_class = device_class + self._attr_state_class = state_class + self._attr_native_unit_of_measurement = unit + self._attr_icon = icon + + @property + def id(self): + """Return the ID of the sensor.""" + return f"{DOMAIN}_{self.entity_name}_{self.device_index}_sensor" + + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return self.id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.coordinator.data[self.device_index]['name']} {self._display_name}" + + @property + def native_value(self): + """Return the state of the sensor.""" + try: + # First try accessing the value directly as it might be a sensor key + value = self.coordinator.data[self.device_index][self.entity_name] + if isinstance(value, dict) and "value" in value: + value = value["value"] + + # Attempt to convert to a number if possible + try: + if isinstance(value, str) and "." in value: + return float(value) + elif isinstance(value, str): + return int(value) + return value + except (ValueError, TypeError): + return value + except (KeyError, TypeError, ValueError) as e: + _LOGGER.debug(f"Error getting value for {self.entity_name}: {e}") + return None \ No newline at end of file From eb1c6c3ae4c5fe2a988995cbd379ab306055337d Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Tue, 8 Jul 2025 18:00:26 +0200 Subject: [PATCH 03/11] Put all keys in the system --- custom_components/brink_ventilation/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/brink_ventilation/__init__.py b/custom_components/brink_ventilation/__init__.py index bcc1dde..fa56171 100644 --- a/custom_components/brink_ventilation/__init__.py +++ b/custom_components/brink_ventilation/__init__.py @@ -108,8 +108,7 @@ async def async_get_devices(hass: HomeAssistant, entry: ConfigEntry, brink_clien # Add any additional sensors (CO2, temperature, humidity, etc.) for key, value in description.items(): - if key not in ["ventilation", "mode", "filters_need_change"]: - system[key] = value + system[key] = value hass.data[DOMAIN][entry.entry_id][DATA_DEVICES] = systems From d0e6417d1a87aecb869c25b1a2ba1474c8079bd1 Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Tue, 8 Jul 2025 18:02:04 +0200 Subject: [PATCH 04/11] Removed sensors that were not available in logs --- .../brink_ventilation/core/brink_home_cloud.py | 10 ---------- custom_components/brink_ventilation/sensor.py | 16 +--------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/custom_components/brink_ventilation/core/brink_home_cloud.py b/custom_components/brink_ventilation/core/brink_home_cloud.py index 4ff096f..e05a43c 100644 --- a/custom_components/brink_ventilation/core/brink_home_cloud.py +++ b/custom_components/brink_ventilation/core/brink_home_cloud.py @@ -151,16 +151,6 @@ async def get_description_values(self, system_id, gateway_id): if "PPM eBus CO2-sensor" in param_name or "PPM CO2-sensor" in param_name: _LOGGER.debug(f"Found CO2 sensor: {param_name}") description_result[param_name] = self.__get_type(param) - - # Add temperature sensors - elif "temperatur" in param_name.lower(): - _LOGGER.debug(f"Found temperature sensor: {param_name}") - description_result[param_name] = self.__get_type(param) - - # Add humidity sensors - elif "feuchte" in param_name.lower(): - _LOGGER.debug(f"Found humidity sensor: {param_name}") - description_result[param_name] = self.__get_type(param) _LOGGER.debug( "get_description_values result: %s", diff --git a/custom_components/brink_ventilation/sensor.py b/custom_components/brink_ventilation/sensor.py index 3f0c126..f0c78ef 100644 --- a/custom_components/brink_ventilation/sensor.py +++ b/custom_components/brink_ventilation/sensor.py @@ -32,20 +32,6 @@ "icon": "mdi:molecule-co2", "patterns": ["PPM eBus CO2-sensor", "PPM CO2-sensor", "CO2-sensor"], }, - "temperature": { - "device_class": SensorDeviceClass.TEMPERATURE, - "state_class": SensorStateClass.MEASUREMENT, - "unit": UnitOfTemperature.CELSIUS, - "icon": "mdi:thermometer", - "patterns": ["temperatur", "Frischlufttemperatur", "Zulufttemperatur"], - }, - "humidity": { - "device_class": SensorDeviceClass.HUMIDITY, - "state_class": SensorStateClass.MEASUREMENT, - "unit": "%", - "icon": "mdi:water-percent", - "patterns": ["Feuchte", "humidity"], - }, } @@ -160,4 +146,4 @@ def native_value(self): return value except (KeyError, TypeError, ValueError) as e: _LOGGER.debug(f"Error getting value for {self.entity_name}: {e}") - return None \ No newline at end of file + return None From 3ed521b485e7ab44cee8d7ac165bf7f2a5c2ac7f Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Tue, 8 Jul 2025 18:25:54 +0200 Subject: [PATCH 05/11] Converted string matching for CO2-sensor to a regex pattern --- .../core/brink_home_cloud.py | 9 ++- custom_components/brink_ventilation/sensor.py | 64 +++++++++---------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/custom_components/brink_ventilation/core/brink_home_cloud.py b/custom_components/brink_ventilation/core/brink_home_cloud.py index e05a43c..3f1702e 100644 --- a/custom_components/brink_ventilation/core/brink_home_cloud.py +++ b/custom_components/brink_ventilation/core/brink_home_cloud.py @@ -1,10 +1,13 @@ """Implementation for Brink-Home Cloud""" import asyncio -import async_timeout import logging +import re + import aiohttp +import async_timeout from ..const import API_URL +from ..sensor import SENSOR_TYPES from ..translations import TRANSLATIONS _LOGGER = logging.getLogger(__name__) @@ -146,9 +149,9 @@ async def get_description_values(self, system_id, gateway_id): # Look for CO2 sensors and other sensors and add them to the result for param in all_parameters: param_name = param.get("name", "") - + # Add CO2 sensors - if "PPM eBus CO2-sensor" in param_name or "PPM CO2-sensor" in param_name: + if re.search(SENSOR_TYPES.get("co2").pattern, param_name): _LOGGER.debug(f"Found CO2 sensor: {param_name}") description_result[param_name] = self.__get_type(param) diff --git a/custom_components/brink_ventilation/sensor.py b/custom_components/brink_ventilation/sensor.py index f0c78ef..7854e65 100644 --- a/custom_components/brink_ventilation/sensor.py +++ b/custom_components/brink_ventilation/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Optional +import re from homeassistant.components.sensor import ( SensorDeviceClass, @@ -10,7 +10,7 @@ SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION, UnitOfTemperature +from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION from homeassistant.core import HomeAssistant from custom_components.brink_ventilation import BrinkHomeDeviceEntity @@ -30,7 +30,7 @@ "state_class": SensorStateClass.MEASUREMENT, "unit": CONCENTRATION_PARTS_PER_MILLION, "icon": "mdi:molecule-co2", - "patterns": ["PPM eBus CO2-sensor", "PPM CO2-sensor", "CO2-sensor"], + "pattern": r"(?=.*\bPPM\b)(?=.*\bCO2\b)", }, } @@ -52,44 +52,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e if isinstance(value, dict) and "name" in value and "value" in value: # Check if the key matches any of our sensor patterns for sensor_type, properties in SENSOR_TYPES.items(): - for pattern in properties["patterns"]: - if pattern.lower() in key.lower(): - _LOGGER.debug(f"Found {sensor_type} sensor by key: {key}") - entities.append( - BrinkSensor( - client, - coordinator, - device_index, - key, - value["name"], - properties["device_class"], - properties["state_class"], - properties["unit"], - properties["icon"] - ) + if re.search(properties["pattern"], key): + _LOGGER.debug(f"Found {sensor_type} sensor by key: {key}") + entities.append( + BrinkSensor( + client, + coordinator, + device_index, + key, + value["name"], + properties["device_class"], + properties["state_class"], + properties["unit"], + properties["icon"] ) + ) # Also check the name field in the value dict as before sensor_name = value.get("name", "") _LOGGER.debug(f"Checking potential sensor: {sensor_name}") for sensor_type, properties in SENSOR_TYPES.items(): - for pattern in properties["patterns"]: - if pattern.lower() in sensor_name.lower(): - _LOGGER.debug(f"Found {sensor_type} sensor by name: {sensor_name}") - entities.append( - BrinkSensor( - client, - coordinator, - device_index, - key, - sensor_name, - properties["device_class"], - properties["state_class"], - properties["unit"], - properties["icon"] - ) + if re.search(properties["pattern"], key): + _LOGGER.debug(f"Found {sensor_type} sensor by name: {sensor_name}") + entities.append( + BrinkSensor( + client, + coordinator, + device_index, + key, + sensor_name, + properties["device_class"], + properties["state_class"], + properties["unit"], + properties["icon"] ) + ) if entities: _LOGGER.info(f"Adding {len(entities)} sensor entities: {[e.entity_name for e in entities]}") From 655c2e6310bb2e6c9d7b6edf86184bce434f0d10 Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Tue, 8 Jul 2025 18:26:03 +0200 Subject: [PATCH 06/11] Ran linter over page --- .../core/brink_home_cloud.py | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/custom_components/brink_ventilation/core/brink_home_cloud.py b/custom_components/brink_ventilation/core/brink_home_cloud.py index 3f1702e..505e0f4 100644 --- a/custom_components/brink_ventilation/core/brink_home_cloud.py +++ b/custom_components/brink_ventilation/core/brink_home_cloud.py @@ -34,7 +34,7 @@ async def _api_call(self, url, method, data=None): "%s request: %s, data %s", method, url, - data + data, ) try: async with async_timeout.timeout(self.timeout): @@ -42,7 +42,7 @@ async def _api_call(self, url, method, data=None): method, url, json=data, - headers=self.headers + headers=self.headers, ) if req.status == 401: @@ -75,7 +75,7 @@ async def login(self): _LOGGER.debug( "login result: %s", - result + result, ) return result @@ -90,62 +90,67 @@ async def get_systems(self): mapped_result = [] for system in result: - mapped_result.append({ - 'system_id': system["id"], - 'gateway_id': system["gatewayId"], - 'name': system['name'] - }) + mapped_result.append( + { + 'system_id': system["id"], + 'gateway_id': system["gatewayId"], + 'name': system['name'] + }, + ) _LOGGER.debug( "get_systems result: %s", - mapped_result + mapped_result, ) return mapped_result async def get_description_values(self, system_id, gateway_id): """Gets values info.""" - url = f"{API_URL}GetAppGuiDescriptionForGateway?GatewayId={gateway_id}&SystemId={system_id}" + url = (f"{API_URL}GetAppGuiDescriptionForGateway?GatewayId=" + f"{gateway_id}&SystemId={system_id}") response = await self._api_call(url, "GET") result = await response.json() _LOGGER.debug( "Response result: %s", - result + result, ) - + menu_items = result.get("menuItems", []) if not menu_items: _LOGGER.debug("No menu items found in API response") return {} - + menu_item = menu_items[0] pages = menu_item.get("pages", []) if not pages: _LOGGER.debug("No pages found in menu item") return {} - + # Extract all parameters from all pages all_parameters = [] for page in pages: parameters = page.get("parameterDescriptors", []) all_parameters.extend(parameters) - + _LOGGER.debug(f"Found {len(all_parameters)} parameters across all pages") # Find the basic parameters ventilation = self.__find(all_parameters, "uiId", "Lüftungsstufe") mode = self.__find(all_parameters, "uiId", "Betriebsart") - filters_need_change = self.__find(all_parameters, "uiId", "Status Filtermeldung") - + filters_need_change = self.__find( + all_parameters, "uiId", "Status Filtermeldung", + ) + # Initialize the result dictionary with the basic parameters description_result = { "ventilation": self.__get_type(ventilation), "mode": self.__get_type(mode), "filters_need_change": self.__get_type(filters_need_change) } - + # Look for CO2 sensors and other sensors and add them to the result for param in all_parameters: param_name = param.get("name", "") @@ -157,7 +162,7 @@ async def get_description_values(self, system_id, gateway_id): _LOGGER.debug( "get_description_values result: %s", - description_result + description_result, ) return description_result @@ -176,15 +181,21 @@ def __get_values(type): extracted = [] for value in values: if value["isSelectable"]: - extracted.append({ - "value": value["value"], - "text": TRANSLATIONS.get(value["displayText"], value["displayText"]) - }) + extracted.append( + { + "value": value["value"], + "text": TRANSLATIONS.get( + value["displayText"], value["displayText"], + ) + }, + ) return extracted # 1 as mode value changes mode to manual every time you change ventilation value - async def set_ventilation_value(self, system_id, gateway_id, mode, ventilation, value): + async def set_ventilation_value( + self, system_id, gateway_id, mode, ventilation, value, + ): ventilation_value = ventilation["values"][value]["value"] if ventilation_value is None: return @@ -230,7 +241,7 @@ async def set_mode_value(self, system_id, gateway_id, mode, value): await self._api_call(url, "POST", data) - def __find(self, arr , attr, value): + def __find(self, arr, attr, value): for obj in arr: try: if obj[attr] == value: @@ -238,6 +249,5 @@ def __find(self, arr , attr, value): except: _LOGGER.debug( "find error: %s", - value + value, ) - From 4ea158a94c0450c8ac0b6c5810af7b3bd7801d24 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 14:59:54 +0000 Subject: [PATCH 07/11] Fix circular import that broke config flow handler - Move SENSOR_TYPES from sensor.py to const.py to break circular import - Change absolute imports to relative imports in all platform files - Fix .pattern attribute access on dict (should be ["pattern"]) The circular import chain was: config_flow.py -> brink_home_cloud.py -> sensor.py -> __init__.py This prevented the config flow handler from being registered, causing "Invalid handler specified" error in Home Assistant. --- .../brink_ventilation/binary_sensor.py | 2 +- custom_components/brink_ventilation/const.py | 13 +++++++++++ .../core/brink_home_cloud.py | 5 ++--- custom_components/brink_ventilation/fan.py | 8 ++----- custom_components/brink_ventilation/select.py | 2 +- custom_components/brink_ventilation/sensor.py | 22 +++---------------- 6 files changed, 22 insertions(+), 30 deletions(-) diff --git a/custom_components/brink_ventilation/binary_sensor.py b/custom_components/brink_ventilation/binary_sensor.py index fc8d74c..a5d0948 100644 --- a/custom_components/brink_ventilation/binary_sensor.py +++ b/custom_components/brink_ventilation/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from custom_components.brink_ventilation import BrinkHomeDeviceEntity +from . import BrinkHomeDeviceEntity from .const import ( DATA_CLIENT, diff --git a/custom_components/brink_ventilation/const.py b/custom_components/brink_ventilation/const.py index 8011ace..5742228 100644 --- a/custom_components/brink_ventilation/const.py +++ b/custom_components/brink_ventilation/const.py @@ -1,4 +1,6 @@ """Constant values for the Brink Home component.""" +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION DOMAIN = "brink_ventilation" DEFAULT_NAME = "Brink" @@ -11,3 +13,14 @@ DEFAULT_SCAN_INTERVAL = 30 API_URL = "https://www.brink-home.com/portal/api/portal/" + +# Define sensor types with their properties +SENSOR_TYPES = { + "co2": { + "device_class": SensorDeviceClass.CO2, + "state_class": SensorStateClass.MEASUREMENT, + "unit": CONCENTRATION_PARTS_PER_MILLION, + "icon": "mdi:molecule-co2", + "pattern": r"(?=.*\bPPM\b)(?=.*\bCO2\b)", + }, +} diff --git a/custom_components/brink_ventilation/core/brink_home_cloud.py b/custom_components/brink_ventilation/core/brink_home_cloud.py index 505e0f4..fe8ad61 100644 --- a/custom_components/brink_ventilation/core/brink_home_cloud.py +++ b/custom_components/brink_ventilation/core/brink_home_cloud.py @@ -6,8 +6,7 @@ import aiohttp import async_timeout -from ..const import API_URL -from ..sensor import SENSOR_TYPES +from ..const import API_URL, SENSOR_TYPES from ..translations import TRANSLATIONS _LOGGER = logging.getLogger(__name__) @@ -156,7 +155,7 @@ async def get_description_values(self, system_id, gateway_id): param_name = param.get("name", "") # Add CO2 sensors - if re.search(SENSOR_TYPES.get("co2").pattern, param_name): + if re.search(SENSOR_TYPES.get("co2")["pattern"], param_name): _LOGGER.debug(f"Found CO2 sensor: {param_name}") description_result[param_name] = self.__get_type(param) diff --git a/custom_components/brink_ventilation/fan.py b/custom_components/brink_ventilation/fan.py index c4956a0..d3c3849 100644 --- a/custom_components/brink_ventilation/fan.py +++ b/custom_components/brink_ventilation/fan.py @@ -3,16 +3,12 @@ import logging import math -from homeassistant.components.fan import ( - DOMAIN, - FanEntity, - FanEntityFeature -) +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.util.percentage import int_states_in_range, ranged_value_to_percentage, percentage_to_ranged_value -from custom_components.brink_ventilation import BrinkHomeDeviceEntity +from . import BrinkHomeDeviceEntity from .const import ( DATA_CLIENT, diff --git a/custom_components/brink_ventilation/select.py b/custom_components/brink_ventilation/select.py index 83fbd62..3dc1115 100644 --- a/custom_components/brink_ventilation/select.py +++ b/custom_components/brink_ventilation/select.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from custom_components.brink_ventilation import BrinkHomeDeviceEntity +from . import BrinkHomeDeviceEntity from .const import ( DATA_CLIENT, diff --git a/custom_components/brink_ventilation/sensor.py b/custom_components/brink_ventilation/sensor.py index 7854e65..d9a9496 100644 --- a/custom_components/brink_ventilation/sensor.py +++ b/custom_components/brink_ventilation/sensor.py @@ -4,36 +4,20 @@ import logging import re -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorStateClass, -) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION from homeassistant.core import HomeAssistant -from custom_components.brink_ventilation import BrinkHomeDeviceEntity - +from . import BrinkHomeDeviceEntity from .const import ( DATA_CLIENT, DATA_COORDINATOR, DOMAIN, + SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) -# Define sensor types with their properties -SENSOR_TYPES = { - "co2": { - "device_class": SensorDeviceClass.CO2, - "state_class": SensorStateClass.MEASUREMENT, - "unit": CONCENTRATION_PARTS_PER_MILLION, - "icon": "mdi:molecule-co2", - "pattern": r"(?=.*\bPPM\b)(?=.*\bCO2\b)", - }, -} - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): """Set up the Brink Home sensor platform.""" From 5d1abbdd1a13bed9e900f84436a0d7486d396bc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 15:01:07 +0000 Subject: [PATCH 08/11] Fix .gitignore to ignore __pycache__ in all directories --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1f1a246..5affc21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /.idea/ -/__pycache__/ \ No newline at end of file +__pycache__/ \ No newline at end of file From 1014a63817567f413985aeaed3197a61c9b9064f Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Thu, 29 Jan 2026 10:31:40 +0100 Subject: [PATCH 09/11] Add bypass damper and operating mode timer sensors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds support for all remaining sensors provided by the Brink API, achieving 100% API coverage. New sensors: - Bypass damper status: Shows if heat recovery bypass is active (open/closed/opening/closing) - Operating mode timer: Shows remaining time in minutes for temporary modes (Party/Holiday/Night) Changes: - Added sensor type definitions for bypass and mode_timer in const.py - Updated brink_home_cloud.py to extract all sensor types using pattern matching - Enhanced sensor.py to translate bypass status values from codes to readable text - Fixed sensor duplication issue by adding break after match The bypass sensor is useful for understanding ventilation behavior in warm weather, and the timer sensor enables better automation and awareness of temporary mode duration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- custom_components/brink_ventilation/const.py | 16 ++++- .../core/brink_home_cloud.py | 11 ++-- custom_components/brink_ventilation/sensor.py | 63 ++++++++----------- 3 files changed, 47 insertions(+), 43 deletions(-) diff --git a/custom_components/brink_ventilation/const.py b/custom_components/brink_ventilation/const.py index 5742228..48ee3dd 100644 --- a/custom_components/brink_ventilation/const.py +++ b/custom_components/brink_ventilation/const.py @@ -1,6 +1,6 @@ """Constant values for the Brink Home component.""" from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION +from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION, UnitOfTime DOMAIN = "brink_ventilation" DEFAULT_NAME = "Brink" @@ -23,4 +23,18 @@ "icon": "mdi:molecule-co2", "pattern": r"(?=.*\bPPM\b)(?=.*\bCO2\b)", }, + "bypass": { + "device_class": None, + "state_class": None, + "unit": None, + "icon": "mdi:valve", + "pattern": r"Status Bypassklappe", + }, + "mode_timer": { + "device_class": SensorDeviceClass.DURATION, + "state_class": None, + "unit": UnitOfTime.MINUTES, + "icon": "mdi:timer-outline", + "pattern": r"Restlaufzeit Betriebsartfunktion", + }, } diff --git a/custom_components/brink_ventilation/core/brink_home_cloud.py b/custom_components/brink_ventilation/core/brink_home_cloud.py index fe8ad61..aca7c69 100644 --- a/custom_components/brink_ventilation/core/brink_home_cloud.py +++ b/custom_components/brink_ventilation/core/brink_home_cloud.py @@ -150,14 +150,15 @@ async def get_description_values(self, system_id, gateway_id): "filters_need_change": self.__get_type(filters_need_change) } - # Look for CO2 sensors and other sensors and add them to the result + # Look for all sensor types and add them to the result for param in all_parameters: param_name = param.get("name", "") - # Add CO2 sensors - if re.search(SENSOR_TYPES.get("co2")["pattern"], param_name): - _LOGGER.debug(f"Found CO2 sensor: {param_name}") - description_result[param_name] = self.__get_type(param) + # Check against all defined sensor types + for sensor_type, properties in SENSOR_TYPES.items(): + if re.search(properties["pattern"], param_name): + _LOGGER.debug(f"Found {sensor_type} sensor: {param_name}") + description_result[param_name] = self.__get_type(param) _LOGGER.debug( "get_description_values result: %s", diff --git a/custom_components/brink_ventilation/sensor.py b/custom_components/brink_ventilation/sensor.py index d9a9496..ab7e0c2 100644 --- a/custom_components/brink_ventilation/sensor.py +++ b/custom_components/brink_ventilation/sensor.py @@ -30,14 +30,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e # Look for CO2 sensors and other specific sensors in the data for device_index, device_data in enumerate(coordinator.data): _LOGGER.debug(f"Device {device_index} available keys: {list(device_data.keys())}") - - # Check for CO2 sensors and other sensors that might be stored as top-level keys + + # Check for sensors that might be stored as top-level keys for key, value in device_data.items(): if isinstance(value, dict) and "name" in value and "value" in value: # Check if the key matches any of our sensor patterns for sensor_type, properties in SENSOR_TYPES.items(): if re.search(properties["pattern"], key): - _LOGGER.debug(f"Found {sensor_type} sensor by key: {key}") + _LOGGER.debug(f"Found {sensor_type} sensor: {key}") entities.append( BrinkSensor( client, @@ -51,27 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e properties["icon"] ) ) - - # Also check the name field in the value dict as before - sensor_name = value.get("name", "") - _LOGGER.debug(f"Checking potential sensor: {sensor_name}") - - for sensor_type, properties in SENSOR_TYPES.items(): - if re.search(properties["pattern"], key): - _LOGGER.debug(f"Found {sensor_type} sensor by name: {sensor_name}") - entities.append( - BrinkSensor( - client, - coordinator, - device_index, - key, - sensor_name, - properties["device_class"], - properties["state_class"], - properties["unit"], - properties["icon"] - ) - ) + break # Only add each sensor once if entities: _LOGGER.info(f"Adding {len(entities)} sensor entities: {[e.entity_name for e in entities]}") @@ -113,19 +93,28 @@ def native_value(self): """Return the state of the sensor.""" try: # First try accessing the value directly as it might be a sensor key - value = self.coordinator.data[self.device_index][self.entity_name] - if isinstance(value, dict) and "value" in value: - value = value["value"] - - # Attempt to convert to a number if possible - try: - if isinstance(value, str) and "." in value: - return float(value) - elif isinstance(value, str): - return int(value) - return value - except (ValueError, TypeError): - return value + data = self.coordinator.data[self.device_index][self.entity_name] + if isinstance(data, dict) and "value" in data: + value = data["value"] + + # For sensors with selectable list items (like bypass status), + # translate the value to the display text + if "values" in data and data["values"]: + for item in data["values"]: + if item.get("value") == value: + return item.get("text", value) + + # Otherwise attempt to convert to a number if possible + try: + if isinstance(value, str) and "." in value: + return float(value) + elif isinstance(value, str): + return int(value) + return value + except (ValueError, TypeError): + return value + else: + return data except (KeyError, TypeError, ValueError) as e: _LOGGER.debug(f"Error getting value for {self.entity_name}: {e}") return None From 1a21495ceea3b1a037822a70bba2f7100d784559 Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Thu, 29 Jan 2026 10:32:57 +0100 Subject: [PATCH 10/11] Update manifest.json for fork version 1.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Bumped version from 1.0.1 to 1.1.0 (new features added) - Updated documentation and issue tracker URLs to point to fork - Added @yunusdemir as codeowner 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- custom_components/brink_ventilation/manifest.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/brink_ventilation/manifest.json b/custom_components/brink_ventilation/manifest.json index d8203fe..0b5baac 100644 --- a/custom_components/brink_ventilation/manifest.json +++ b/custom_components/brink_ventilation/manifest.json @@ -1,14 +1,14 @@ { "domain": "brink_ventilation", "name": "Brink-home Ventilation", - "codeowners": ["@samuolis"], + "codeowners": ["@samuolis", "@yunusdemir"], "config_flow": true, "dependencies": ["http"], - "documentation": "https://github.com/samuolis/brink/blob/master/README.md", + "documentation": "https://github.com/yunusdemir/brink/blob/master/README.md", "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/samuolis/brink/issues", + "issue_tracker": "https://github.com/yunusdemir/brink/issues", "requirements": [], "ssdp": [], - "version": "1.0.1", + "version": "1.1.0", "zeroconf": [] } From ccfd79062da0f32512bc9246868f56971e09960c Mon Sep 17 00:00:00 2001 From: Yunus Demir Date: Thu, 29 Jan 2026 10:50:43 +0100 Subject: [PATCH 11/11] Revert "Update manifest.json for fork version 1.1.0" This reverts commit 1a21495ceea3b1a037822a70bba2f7100d784559. --- custom_components/brink_ventilation/manifest.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/brink_ventilation/manifest.json b/custom_components/brink_ventilation/manifest.json index 0b5baac..d8203fe 100644 --- a/custom_components/brink_ventilation/manifest.json +++ b/custom_components/brink_ventilation/manifest.json @@ -1,14 +1,14 @@ { "domain": "brink_ventilation", "name": "Brink-home Ventilation", - "codeowners": ["@samuolis", "@yunusdemir"], + "codeowners": ["@samuolis"], "config_flow": true, "dependencies": ["http"], - "documentation": "https://github.com/yunusdemir/brink/blob/master/README.md", + "documentation": "https://github.com/samuolis/brink/blob/master/README.md", "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/yunusdemir/brink/issues", + "issue_tracker": "https://github.com/samuolis/brink/issues", "requirements": [], "ssdp": [], - "version": "1.1.0", + "version": "1.0.1", "zeroconf": [] }