From 0fd4f2d76ecfb5763d17119d7116e688448f9e3e Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Tue, 27 Jan 2026 18:22:32 +0100 Subject: [PATCH 1/9] feat: Add full CHOICE.All device support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add proper support for Blanco CHOICE.All devices (dev_type=2) with new sensors, settings, and controls. This addresses issue #6 where CHOICE.All devices connect but return limited data. New features: - Heating Temperature control (60-100°C select entity) - Boiler Temperature sensors (temp_boil_1, temp_boil_2) - Cooling Compartment Temperature sensor - Main/Connection Controller Status sensors - Heating Setpoint sensor - Hot Water Calibration sensor - Medium/Classic Carbonation Ratio sensors Changes: - client.py: Add set_heating_temperature(), parse CHOICE.All fields - coordinator.py: Add set_heating_temperature() with validation - data.py: Add CHOICE.All fields to Settings and Status dataclasses - select.py: Add HeatingTemperatureSelect (60-100°C) - sensor.py: Add 9 new CHOICE.All sensors - translations/en.json: Add translations for all new entities All new fields have defaults for backwards compatibility with drink.soda devices. Tested on real CHOICE.All hardware with HA 2026.1.3. --- custom_components/blanco_unit/client.py | 48 +++- custom_components/blanco_unit/coordinator.py | 17 ++ custom_components/blanco_unit/data.py | 11 + custom_components/blanco_unit/select.py | 32 +++ custom_components/blanco_unit/sensor.py | 251 +++++++++++++++--- .../blanco_unit/translations/en.json | 48 +++- 6 files changed, 371 insertions(+), 36 deletions(-) diff --git a/custom_components/blanco_unit/client.py b/custom_components/blanco_unit/client.py index 61bf8be..67ef715 100644 --- a/custom_components/blanco_unit/client.py +++ b/custom_components/blanco_unit/client.py @@ -121,15 +121,26 @@ def to_dict(self) -> dict[str, Any]: @dataclass class _SetTemperaturePars: - """Internal: Parameters for setting temperature.""" + """Internal: Parameters for setting cooling temperature.""" cooling_celsius: int - heating_celsius: int = 65 def to_pars(self) -> dict[str, Any]: """Convert to parameters dictionary.""" return { "set_point_cooling": {"val": self.cooling_celsius}, + } + + +@dataclass +class _SetHeatingTemperaturePars: + """Internal: Parameters for setting heating temperature (CHOICE.All only).""" + + heating_celsius: int + + def to_pars(self) -> dict[str, Any]: + """Convert to parameters dictionary.""" + return { "set_point_heating": {"val": self.heating_celsius}, } @@ -555,6 +566,11 @@ async def get_settings(self) -> BlancoUnitSettings: post_flush_quantity=pars.get("post_flush_quantity", {}).get("val", 0), set_point_cooling=pars.get("set_point_cooling", {}).get("val", 0), wtr_hardness=pars.get("wtr_hardness", {}).get("val", 0), + # CHOICE.All specific fields + set_point_heating=pars.get("set_point_heating", {}).get("val", 0), + calib_hot_wtr=pars.get("calib_hot_wtr", {}).get("val", 0), + gbl_medium_wtr_ratio=pars.get("gbl_medium_wtr_ratio", {}).get("val", 0.0), + gbl_classic_wtr_ratio=pars.get("gbl_classic_wtr_ratio", {}).get("val", 0.0), ) async def get_status(self) -> BlancoUnitStatus: @@ -570,6 +586,12 @@ async def get_status(self) -> BlancoUnitStatus: set_point_cooling=pars.get("set_point_cooling", {}).get("val", 0), clean_mode_state=pars.get("clean_mode_state", {}).get("val", 0), err_bits=pars.get("err_bits", {}).get("val", 0), + # CHOICE.All specific fields + temp_boil_1=pars.get("temp_boil_1", {}).get("val", 0), + temp_boil_2=pars.get("temp_boil_2", {}).get("val", 0), + temp_comp=pars.get("temp_comp", {}).get("val", 0), + main_controller_status=pars.get("main_controller_status", {}).get("val", 0), + conn_controller_status=pars.get("conn_controller_status", {}).get("val", 0), ) async def get_device_identity(self) -> BlancoUnitIdentity: @@ -616,11 +638,31 @@ async def set_temperature(self, cooling_celsius: int) -> bool: if not (4 <= cooling_celsius <= 10): raise ValueError("Temperature must be between 4 and 10°C") - _LOGGER.info("Setting temperature to %d°C", cooling_celsius) + _LOGGER.info("Setting cooling temperature to %d°C", cooling_celsius) req = _SetTemperaturePars(cooling_celsius=cooling_celsius) resp = await self._execute_transaction(evt_type=7, ctrl=5, pars=req.to_pars()) return resp.get("type") == 2 + async def set_heating_temperature(self, heating_celsius: int) -> bool: + """Set heating/boiling temperature (85-100°C, CHOICE.All only). + + Args: + heating_celsius: Target heating temperature in Celsius (85-100). + + Returns: + True if successful. + + Raises: + ValueError: If temperature is out of range. + """ + if not (60 <= heating_celsius <= 100): + raise ValueError("Heating temperature must be between 60 and 100°C") + + _LOGGER.info("Setting heating temperature to %d°C", heating_celsius) + req = _SetHeatingTemperaturePars(heating_celsius=heating_celsius) + resp = await self._execute_transaction(evt_type=7, ctrl=5, pars=req.to_pars()) + return resp.get("type") == 2 + async def set_water_hardness(self, level: int) -> bool: """Set water hardness level (1-9). diff --git a/custom_components/blanco_unit/coordinator.py b/custom_components/blanco_unit/coordinator.py index 0acb6d6..6cc6423 100644 --- a/custom_components/blanco_unit/coordinator.py +++ b/custom_components/blanco_unit/coordinator.py @@ -143,6 +143,23 @@ async def set_temperature(self, cooling_celsius: int) -> None: }, ) + async def set_heating_temperature(self, heating_celsius: int) -> None: + """Set target heating temperature (85-100°C, CHOICE.All only).""" + await self._call(self._client.set_heating_temperature, heating_celsius) + # Refresh settings to verify the change + settings = await self._call(self._client.get_settings) + if self.data is not None: + self.async_set_updated_data(replace(self.data, settings=settings)) + if settings.set_point_heating != heating_celsius: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="not_saved_heating_temperature", + translation_placeholders={ + "expected": str(heating_celsius), + "actual": str(settings.set_point_heating), + }, + ) + async def set_water_hardness(self, level: int) -> None: """Set water hardness level (1-9).""" await self._call(self._client.set_water_hardness, level) diff --git a/custom_components/blanco_unit/data.py b/custom_components/blanco_unit/data.py index abd27af..d0a3e1a 100644 --- a/custom_components/blanco_unit/data.py +++ b/custom_components/blanco_unit/data.py @@ -26,6 +26,11 @@ class BlancoUnitSettings: post_flush_quantity: int set_point_cooling: int wtr_hardness: int + # CHOICE.All specific fields (defaults for drink.soda compatibility) + set_point_heating: int = 0 + calib_hot_wtr: int = 0 + gbl_medium_wtr_ratio: float = 0.0 + gbl_classic_wtr_ratio: float = 0.0 @dataclass @@ -40,6 +45,12 @@ class BlancoUnitStatus: set_point_cooling: int clean_mode_state: int err_bits: int + # CHOICE.All specific fields (defaults for drink.soda compatibility) + temp_boil_1: int = 0 + temp_boil_2: int = 0 + temp_comp: int = 0 + main_controller_status: int = 0 + conn_controller_status: int = 0 @dataclass diff --git a/custom_components/blanco_unit/select.py b/custom_components/blanco_unit/select.py index a1cb7b9..197a071 100644 --- a/custom_components/blanco_unit/select.py +++ b/custom_components/blanco_unit/select.py @@ -22,6 +22,7 @@ async def async_setup_entry( async_add_entities( [ TemperatureSelect(coordinator), + HeatingTemperatureSelect(coordinator), WaterHardnessSelect(coordinator), ] ) @@ -57,6 +58,37 @@ async def async_select_option(self, option: str) -> None: await self.coordinator.set_temperature(int(option)) +class HeatingTemperatureSelect(BlancoUnitBaseEntity, SelectEntity): + """Implementation of the Heating Temperature Selector (60-100°C, CHOICE.All only).""" + + _attr_unique_id = "heating_temperature" + _attr_translation_key = _attr_unique_id + _attr_options = [str(t) for t in range(60, 101)] # 60-100°C + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_entity_category = EntityCategory.CONFIG + _attr_unit_of_measurement = UnitOfTemperature.CELSIUS + + @property + def available(self) -> bool: + """Set availability if settings are available and device supports heating.""" + return ( + super().available + and self.coordinator.data.settings is not None + and self.coordinator.data.settings.set_point_heating > 0 + ) + + @property + def current_option(self) -> str | None: + """Return the current heating temperature setting.""" + if self.coordinator.data.settings is None: + return None + return str(self.coordinator.data.settings.set_point_heating) + + async def async_select_option(self, option: str) -> None: + """Select a heating temperature option.""" + await self.coordinator.set_heating_temperature(int(option)) + + class WaterHardnessSelect(BlancoUnitBaseEntity, SelectEntity): """Implementation of the Water Hardness Selector (1-9).""" diff --git a/custom_components/blanco_unit/sensor.py b/custom_components/blanco_unit/sensor.py index f0209da..3d03206 100644 --- a/custom_components/blanco_unit/sensor.py +++ b/custom_components/blanco_unit/sensor.py @@ -9,6 +9,7 @@ PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, + UnitOfTemperature, UnitOfTime, ) from homeassistant.core import HomeAssistant @@ -35,17 +36,26 @@ async def async_setup_entry( TapStateSensor(coordinator), CleanModeStateSensor(coordinator), ErrorBitsSensor(coordinator), + # CHOICE.All status sensors + BoilerTemp1Sensor(coordinator), + BoilerTemp2Sensor(coordinator), + CoolingTempSensor(coordinator), + MainControllerStatusSensor(coordinator), + ConnControllerStatusSensor(coordinator), # Settings sensors FilterLifetimeSensor(coordinator), PostFlushQuantitySensor(coordinator), + # CHOICE.All settings sensors + HeatingSetpointSensor(coordinator), + HotWaterCalibrationSensor(coordinator), + MediumCarbonationRatioSensor(coordinator), + ClassicCarbonationRatioSensor(coordinator), # System info sensors FirmwareMainSensor(coordinator), FirmwareCommSensor(coordinator), FirmwareElecSensor(coordinator), DeviceNameSensor(coordinator), ResetCountSensor(coordinator), - DeviceTypeSensor(coordinator), - DeviceIdSensor(coordinator), # Identity sensors SerialNumberSensor(coordinator), ServiceCodeSensor(coordinator), @@ -174,6 +184,122 @@ def native_value(self) -> int | None: return self.coordinator.data.status.err_bits +# ------------------------------- +# CHOICE.All Status Sensors +# ------------------------------- + + +class BoilerTemp1Sensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for boiler temperature 1 (CHOICE.All only).""" + + _attr_unique_id = "boiler_temp_1" + _attr_translation_key = _attr_unique_id + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_icon = "mdi:thermometer-water" + + @property + def available(self) -> bool: + """Set availability if status is available.""" + return super().available and self.coordinator.data.status is not None + + @property + def native_value(self) -> int | None: + """Return the boiler temperature 1.""" + if self.coordinator.data.status is None: + return None + return self.coordinator.data.status.temp_boil_1 + + +class BoilerTemp2Sensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for boiler temperature 2 (CHOICE.All only).""" + + _attr_unique_id = "boiler_temp_2" + _attr_translation_key = _attr_unique_id + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_icon = "mdi:thermometer-water" + + @property + def available(self) -> bool: + """Set availability if status is available.""" + return super().available and self.coordinator.data.status is not None + + @property + def native_value(self) -> int | None: + """Return the boiler temperature 2.""" + if self.coordinator.data.status is None: + return None + return self.coordinator.data.status.temp_boil_2 + + +class CoolingTempSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for cooling compartment temperature (CHOICE.All only).""" + + _attr_unique_id = "cooling_temp" + _attr_translation_key = _attr_unique_id + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_icon = "mdi:snowflake-thermometer" + + @property + def available(self) -> bool: + """Set availability if status is available.""" + return super().available and self.coordinator.data.status is not None + + @property + def native_value(self) -> int | None: + """Return the cooling compartment temperature.""" + if self.coordinator.data.status is None: + return None + return self.coordinator.data.status.temp_comp + + +class MainControllerStatusSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for main controller status (CHOICE.All only).""" + + _attr_unique_id = "main_controller_status" + _attr_translation_key = _attr_unique_id + _attr_icon = "mdi:chip" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def available(self) -> bool: + """Set availability if status is available.""" + return super().available and self.coordinator.data.status is not None + + @property + def native_value(self) -> int | None: + """Return the main controller status.""" + if self.coordinator.data.status is None: + return None + return self.coordinator.data.status.main_controller_status + + +class ConnControllerStatusSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for connection controller status (CHOICE.All only).""" + + _attr_unique_id = "conn_controller_status" + _attr_translation_key = _attr_unique_id + _attr_icon = "mdi:chip" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def available(self) -> bool: + """Set availability if status is available.""" + return super().available and self.coordinator.data.status is not None + + @property + def native_value(self) -> int | None: + """Return the connection controller status.""" + if self.coordinator.data.status is None: + return None + return self.coordinator.data.status.conn_controller_status + + # ------------------------------- # Settings Sensors # ------------------------------- @@ -225,6 +351,99 @@ def native_value(self) -> int | None: return self.coordinator.data.settings.post_flush_quantity +# ------------------------------- +# CHOICE.All Settings Sensors +# ------------------------------- + + +class HeatingSetpointSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for heating setpoint temperature (CHOICE.All only).""" + + _attr_unique_id = "heating_setpoint" + _attr_translation_key = _attr_unique_id + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_icon = "mdi:thermometer-high" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def available(self) -> bool: + """Set availability if settings are available.""" + return super().available and self.coordinator.data.settings is not None + + @property + def native_value(self) -> int | None: + """Return the heating setpoint temperature.""" + if self.coordinator.data.settings is None: + return None + return self.coordinator.data.settings.set_point_heating + + +class HotWaterCalibrationSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for hot water calibration (CHOICE.All only).""" + + _attr_unique_id = "hot_water_calibration" + _attr_translation_key = _attr_unique_id + _attr_device_class = SensorDeviceClass.VOLUME + _attr_icon = "mdi:water-thermometer" + _attr_native_unit_of_measurement = "mL" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def available(self) -> bool: + """Set availability if settings are available.""" + return super().available and self.coordinator.data.settings is not None + + @property + def native_value(self) -> int | None: + """Return the hot water calibration.""" + if self.coordinator.data.settings is None: + return None + return self.coordinator.data.settings.calib_hot_wtr + + +class MediumCarbonationRatioSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for medium carbonation water ratio (CHOICE.All only).""" + + _attr_unique_id = "medium_carbonation_ratio" + _attr_translation_key = _attr_unique_id + _attr_icon = "mdi:gas-cylinder" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def available(self) -> bool: + """Set availability if settings are available.""" + return super().available and self.coordinator.data.settings is not None + + @property + def native_value(self) -> float | None: + """Return the medium carbonation water ratio.""" + if self.coordinator.data.settings is None: + return None + return self.coordinator.data.settings.gbl_medium_wtr_ratio + + +class ClassicCarbonationRatioSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for classic carbonation water ratio (CHOICE.All only).""" + + _attr_unique_id = "classic_carbonation_ratio" + _attr_translation_key = _attr_unique_id + _attr_icon = "mdi:gas-cylinder" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def available(self) -> bool: + """Set availability if settings are available.""" + return super().available and self.coordinator.data.settings is not None + + @property + def native_value(self) -> float | None: + """Return the classic carbonation water ratio.""" + if self.coordinator.data.settings is None: + return None + return self.coordinator.data.settings.gbl_classic_wtr_ratio + + # ------------------------------- # System Info Sensors # ------------------------------- @@ -336,34 +555,6 @@ def native_value(self) -> int | None: return self.coordinator.data.system_info.reset_cnt -class DeviceTypeSensor(BlancoUnitBaseEntity, SensorEntity): - """Sensor for device type.""" - - _attr_unique_id = "device_type" - _attr_translation_key = _attr_unique_id - _attr_icon = "mdi:information" - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def native_value(self) -> int | None: - """Return the device type.""" - return self.coordinator.data.device_type - - -class DeviceIdSensor(BlancoUnitBaseEntity, SensorEntity): - """Sensor for device id.""" - - _attr_unique_id = "device_id" - _attr_translation_key = _attr_unique_id - _attr_icon = "mdi:information" - _attr_entity_category = EntityCategory.DIAGNOSTIC - - @property - def native_value(self) -> int | None: - """Return the device id.""" - return self.coordinator.data.device_id - - # ------------------------------- # Identity Sensors # ------------------------------- diff --git a/custom_components/blanco_unit/translations/en.json b/custom_components/blanco_unit/translations/en.json index b9ecac6..d77fa8b 100644 --- a/custom_components/blanco_unit/translations/en.json +++ b/custom_components/blanco_unit/translations/en.json @@ -21,6 +21,9 @@ "not_saved_temperature": { "message": "Temperature change was not saved on the device. Expected {expected}°C but device shows {actual}°C." }, + "not_saved_heating_temperature": { + "message": "Heating temperature change was not saved on the device. Expected {expected}°C but device shows {actual}°C." + }, "not_saved_water_hardness": { "message": "Water hardness change was not saved on the device. Expected level {expected} but device shows level {actual}." } @@ -123,6 +126,21 @@ "10": "10°C (warmest)" } }, + "heating_temperature": { + "name": "Heating Temperature", + "state": { + "60": "60°C", + "65": "65°C", + "70": "70°C", + "75": "75°C", + "80": "80°C", + "85": "85°C", + "90": "90°C", + "95": "95°C", + "99": "99°C (recommended)", + "100": "100°C (boiling)" + } + }, "water_hardness": { "name": "Water Hardness Level", "state": { @@ -154,12 +172,39 @@ "error_bits": { "name": "Error Code" }, + "boiler_temp_1": { + "name": "Boiler Temperature 1" + }, + "boiler_temp_2": { + "name": "Boiler Temperature 2" + }, + "cooling_temp": { + "name": "Cooling Compartment Temperature" + }, + "main_controller_status": { + "name": "Main Controller Status" + }, + "conn_controller_status": { + "name": "Connection Controller Status" + }, "filter_lifetime": { "name": "Filter Lifetime" }, "post_flush_quantity": { "name": "Post-Flush Quantity" }, + "heating_setpoint": { + "name": "Heating Setpoint" + }, + "hot_water_calibration": { + "name": "Hot Water Calibration" + }, + "medium_carbonation_ratio": { + "name": "Medium Carbonation Ratio" + }, + "classic_carbonation_ratio": { + "name": "Classic Carbonation Ratio" + }, "firmware_main": { "name": "Main Controller Firmware" }, @@ -175,9 +220,6 @@ "reset_count": { "name": "Reset Count" }, - "device_type": { - "name": "Device Type" - }, "serial_number": { "name": "Serial Number" }, From c92ac531eb002e39125d79485ecec3ecf7096f34 Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Tue, 27 Jan 2026 18:49:43 +0100 Subject: [PATCH 2/9] Address review feedback - Add entity_registry_visible_default to hide CHOICE.All entities on drink.soda - Restore DeviceTypeSensor and DeviceIdSensor - Update test for split temperature params classes - Add translations for device_type and device_id sensors --- .coverage | Bin 53248 -> 53248 bytes custom_components/blanco_unit/select.py | 5 ++ custom_components/blanco_unit/sensor.py | 75 ++++++++++++++++++ .../blanco_unit/translations/en.json | 6 ++ tests/test_client.py | 9 ++- 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/.coverage b/.coverage index 9c5e334d7fa93080f0b13e51e95c8b540ba6e727..62056c47250fa25a6cf71d69d8e0a685298d4400 100644 GIT binary patch delta 1799 zcma))Z%7ki9LJyAp6zzt-Scdjm?&;Ldl4?TLE(f*dJ#J3pV>RJ@=#kavbo$8HF7%$ zdJ#f#udEl2-UNk0sgP8*-t;0PSP}KYm=*|TsV@{uv-NCJ`R=)cyMy0-?#K7{{66u7 zKAzCu?9dHwJv@KcYFq$#6)#L!%0bXjv5GYnxxF^`x9u2snm%^@&uJ%x8x7-^F zMrF6VQdJu5YIB_+)gXdeD5zB(Z(t>_NYDZr>WBoo0-aH3c)%|ZzKq7J&IYsOD?D3V=&LUYN#+AP2PzO8Qb=;XbIq-cov_+HTVcN zq9vm1I}|HsA;VR%t%blh-~Hppb+G4(JB;LGi*c*+sq!j;b6>(IZ8jZ%zy!>YVa#E` zkg}QFCX0c<2My($)9Fa2+Os*jjX6k4`(b%(@mIgzz;H@f|25{ZxgC!AnW2@fX*m`< zJN)2s@A#XsxxAJSQ)gbvQ_o^Mt?A6MPoc??jn$RfJ4NpG_|=QwPAqTV8d^rlvB8}$ z`Ro47@tyQNe+I*38vroL$Df0>D~5iiw1h;NNylH%RVL<;uR|*$SI002DSbkmHapFI&SYfVc5F{*D)M8h^woa%fNRL~7LK{sV@d BRC)ja literal 53248 zcmeI4du$ZP8NhdM?{07JHLeI;kW0Lclo*5UvzrPMkf5|sl{PA*K#)pd**mY#8{h4k z+dUf-z&WUbng~^G8mW~C@LwvbKorXmNO)91L zcP}7jhn00Y{ptEH~{0Xhcyn8Z9A?cdFNkd8Jj!D_UAHY%RvCEp_3#wLM`zF=`aMPO)r~ z^J#cRDy)HovMIA$p`o;%Z=u?!^0Jn0Q;d31E?Jn?LC@u;ZEar9B~4w-s+wh{W~ipB zYofATNf%6|<+cXaQ)uo1@P=ChZ7rn+EwyVpvWC$qSH4Y6r?^wO6_nF-7i+m96%Caw zowY(P1<0Q&V<~DsMUIioZ>x%Ra1MLQC> zPO}h9%&Hm5bxJNEHJ47q#j!y?QC-ch5iB=yddrFU3Kw#F@;1|`o0U^!qmXwelFynX z-5TXt(j6mC4w%!x2NN@@M>5ALCp9}Kp(5|+6Qf77MSDchd5OLQ6-LmM@({RbA^802 zcgSX&c&qL-85FM#1QSz7kEA$NL29O9aP19n zUABxVZmL+@CL44XGHS(?u4fflv)2ZwG1~x{sv%lB+`1}FFSlWI(>5}lwU$oq;zFnS z`7r3*@dYQjoUFAt>lIyw?N(v#r-PHqu_qCzDAvNDG+>C{)TZ00lS^kTxBbQrU_x8X z$u0J9r=2pa#jZBxmgel$Xr^o`rkYbkTEtSua;z@d5Yu@DGGQ&PtK~k+4QOvk%`~LD z&0VIF~kN^@u0!RP}AOR$R1dsp{xGM=necU+W{==iGish=uI{yKf z+}PaI*c1kjBd_>OQl?I~HNB{{S0VIF~kN^@u0!RP}AOR$R1du?9K$IKDx?cnM z!d$iBd<#H-|0khi4EZ@36MrfGP;6_gDcTpE7TFV-72Y5If$)LwW5~t>2_OL^fCP{L z5RoC1$kNY2gc zPbq2BNT#yz%bo?dTR&&S<(}s%e9o9ro7jRKbL2OMu|jf}dH# zdgoy&Rf8v^NF_%rgurm!L;13yfZ5$Vi0$%DEKD4oDm>SPI$F?MM@#G0qhMsy8rX$^ zx4f+z@;kaa{UCOkcf4U5HLU4`1Gyefpy{j%Q#y4p9|-o)!6ZqAg+`JB*2 zpCwlrc;SHrkN^@u0!RP}AOR$R1dsp{Kmter3EYDOI4&kw_y5Oz47o(!A@j(0$s`hv z55|wg-;Vdix5R%He>VON$i@Q+AOR$R1dsp{Kmter2_OL^fCTP(0!_rv3Mq2(=vww) z8yD#PpI-X((&i1_&mXw|#Ngn8Pa108*luk4!}*nGc06`=@`d*{T!_t$2Uwvf#>2ns zKeXrC>6(3e`g--#AHVX_?!Mj~o%>d|XZH4SLkIhh{q^vosZY0O1|S|9^XiVyp@Z=c z7frq56fBqEt74$TgR#r~hu&H}^^Mo`%)+WDO^L1;JSE?FW%1#j_Ju+OQtpdvS-h2t zh2iw8VVbGGxZ5{cfXjN}xv2v+LkC|wa`^C$-d;V!heMD&CUmm(bnlq2@o+wq|51YZ z-k^T*`E~i-D?T|pf%e^|13#N~CNK1o0Efo-ygVmZt=FwCwu;K z<2PG&O?mXuXRhlPj-A>waQ1YdDgb343{1GNbJLZ<6W5I!tl)=~`}`AnPFyuOil>!3 z*)v&enmF^6x&H6z*EUVNHob~@n*EROc#^rggkACs&x77wzN@2- zzrZp3zW&E9?hKpsf&8VuE!7X)zwh$wfnRb>1IDgRnlzB^`>gx_$*&o5g`6X&$q6z*J|ypxcgb7iP0~lUl9$P5$i@Q+AOR$R z1dsp{Kmter2_OL^fCP{L61Wot=(iCpAyyQRTTv`#MbW4gMIu%d4qK5RSWzfsMLcgs z!Jrid0#@YrTM@@ukgVF0#?1dsp{Kmter2_OL^fCP{L5PwR$bZSlWRU!W z9EE!T{!IGG@5$R_FWCjzcpw2JfCP{L5 bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if settings are available and device supports heating.""" diff --git a/custom_components/blanco_unit/sensor.py b/custom_components/blanco_unit/sensor.py index 3d03206..35c5236 100644 --- a/custom_components/blanco_unit/sensor.py +++ b/custom_components/blanco_unit/sensor.py @@ -56,6 +56,8 @@ async def async_setup_entry( FirmwareElecSensor(coordinator), DeviceNameSensor(coordinator), ResetCountSensor(coordinator), + DeviceTypeSensor(coordinator), + DeviceIdSensor(coordinator), # Identity sensors SerialNumberSensor(coordinator), ServiceCodeSensor(coordinator), @@ -199,6 +201,11 @@ class BoilerTemp1Sensor(BlancoUnitBaseEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:thermometer-water" + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if status is available.""" @@ -222,6 +229,11 @@ class BoilerTemp2Sensor(BlancoUnitBaseEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:thermometer-water" + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if status is available.""" @@ -245,6 +257,11 @@ class CoolingTempSensor(BlancoUnitBaseEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:snowflake-thermometer" + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if status is available.""" @@ -266,6 +283,11 @@ class MainControllerStatusSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:chip" _attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if status is available.""" @@ -287,6 +309,11 @@ class ConnControllerStatusSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:chip" _attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if status is available.""" @@ -366,6 +393,11 @@ class HeatingSetpointSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:thermometer-high" _attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if settings are available.""" @@ -389,6 +421,11 @@ class HotWaterCalibrationSensor(BlancoUnitBaseEntity, SensorEntity): _attr_native_unit_of_measurement = "mL" _attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if settings are available.""" @@ -410,6 +447,11 @@ class MediumCarbonationRatioSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:gas-cylinder" _attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if settings are available.""" @@ -431,6 +473,11 @@ class ClassicCarbonationRatioSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:gas-cylinder" _attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + @property def available(self) -> bool: """Set availability if settings are available.""" @@ -555,6 +602,34 @@ def native_value(self) -> int | None: return self.coordinator.data.system_info.reset_cnt +class DeviceTypeSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for device type.""" + + _attr_unique_id = "device_type" + _attr_translation_key = _attr_unique_id + _attr_icon = "mdi:information" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def native_value(self) -> int | None: + """Return the device type.""" + return self.coordinator.data.device_type + + +class DeviceIdSensor(BlancoUnitBaseEntity, SensorEntity): + """Sensor for device id.""" + + _attr_unique_id = "device_id" + _attr_translation_key = _attr_unique_id + _attr_icon = "mdi:information" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def native_value(self) -> str | None: + """Return the device id.""" + return self.coordinator.data.device_id + + # ------------------------------- # Identity Sensors # ------------------------------- diff --git a/custom_components/blanco_unit/translations/en.json b/custom_components/blanco_unit/translations/en.json index d77fa8b..cced730 100644 --- a/custom_components/blanco_unit/translations/en.json +++ b/custom_components/blanco_unit/translations/en.json @@ -220,6 +220,12 @@ "reset_count": { "name": "Reset Count" }, + "device_type": { + "name": "Device Type" + }, + "device_id": { + "name": "Device ID" + }, "serial_number": { "name": "Serial Number" }, diff --git a/tests/test_client.py b/tests/test_client.py index ba860ad..973c827 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -22,6 +22,7 @@ _RequestEnvelope, _RequestMeta, _SetCalibrationPars, + _SetHeatingTemperaturePars, _SetTemperaturePars, _SetWaterHardnessPars, validate_pin, @@ -132,9 +133,15 @@ def test_request_envelope_to_dict(): def test_set_temperature_pars_to_pars(): """Test _SetTemperaturePars.to_pars().""" - pars = _SetTemperaturePars(cooling_celsius=7, heating_celsius=65) + pars = _SetTemperaturePars(cooling_celsius=7) result = pars.to_pars() assert result["set_point_cooling"]["val"] == 7 + + +def test_set_heating_temperature_pars_to_pars(): + """Test _SetHeatingTemperaturePars.to_pars().""" + pars = _SetHeatingTemperaturePars(heating_celsius=65) + result = pars.to_pars() assert result["set_point_heating"]["val"] == 65 From a9011b6ab4ebc2af30439c205cfa63358f76d224 Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Tue, 27 Jan 2026 23:12:14 +0100 Subject: [PATCH 3/9] feat: Add heater/compressor binary sensors from status bitmask Decode main_controller_status bitmask into meaningful binary sensors: - Heater Active (bit 13/0x2000): Boiler heater element is running - Compressor Active (bit 14/0x4000): Cooling compressor is running Analysis of real device data shows: - Bit 14 correlates with cooling cycles (~15 min intervals) - Bit 13 correlates with heating cycles (~40 min intervals) - Both bits are never set simultaneously (load management) Hidden by default on drink.soda devices (only visible for CHOICE.All). --- .../blanco_unit/binary_sensor.py | 74 +++++++++++++++++++ .../blanco_unit/translations/en.json | 6 ++ 2 files changed, 80 insertions(+) diff --git a/custom_components/blanco_unit/binary_sensor.py b/custom_components/blanco_unit/binary_sensor.py index 429f28f..4589c3e 100644 --- a/custom_components/blanco_unit/binary_sensor.py +++ b/custom_components/blanco_unit/binary_sensor.py @@ -13,6 +13,12 @@ from .coordinator import BlancoUnitCoordinator +# Main controller status bitmask constants (CHOICE.All) +# Base state (bits 8 + 16) = 65792 is always set when device is running +STATUS_BIT_HEATER = 0x2000 # Bit 13 (8192) - Boiler heater element active +STATUS_BIT_COMPRESSOR = 0x4000 # Bit 14 (16384) - Cooling compressor active + + async def async_setup_entry( _: HomeAssistant, config_entry: BlancoUnitConfigEntry, @@ -26,6 +32,9 @@ async def async_setup_entry( WaterDispensingBinarySensor(coordinator), FirmwareUpdateBinarySensor(coordinator), CloudConnectBinarySensor(coordinator), + # CHOICE.All status binary sensors (decoded from main_controller_status) + HeaterActiveBinarySensor(coordinator), + CompressorActiveBinarySensor(coordinator), ] ) @@ -120,3 +129,68 @@ def is_on(self) -> bool | None: if self.coordinator.data.wifi_info is None: return None return self.coordinator.data.wifi_info.cloud_connect + + +class HeaterActiveBinarySensor(BlancoUnitBaseEntity, BinarySensorEntity): + """Sensor to indicate if the boiler heater is active (CHOICE.All only). + + Decoded from main_controller_status bit 13 (0x2000). + When active, the boiler heater element is on to heat water to setpoint. + """ + + _attr_unique_id = "heater_active" + _attr_translation_key = _attr_unique_id + _attr_device_class = BinarySensorDeviceClass.RUNNING + _attr_icon = "mdi:fire" + + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + + @property + def available(self) -> bool: + """Set availability if status is available.""" + return super().available and self.coordinator.data.status is not None + + @property + def is_on(self) -> bool | None: + """Return if the heater is currently active.""" + if self.coordinator.data.status is None: + return None + return bool( + self.coordinator.data.status.main_controller_status & STATUS_BIT_HEATER + ) + + +class CompressorActiveBinarySensor(BlancoUnitBaseEntity, BinarySensorEntity): + """Sensor to indicate if the cooling compressor is active (CHOICE.All only). + + Decoded from main_controller_status bit 14 (0x4000). + When active, the compressor is running to cool the water compartment. + Note: Heater and compressor never run simultaneously (load management). + """ + + _attr_unique_id = "compressor_active" + _attr_translation_key = _attr_unique_id + _attr_device_class = BinarySensorDeviceClass.RUNNING + _attr_icon = "mdi:snowflake" + + @property + def entity_registry_visible_default(self) -> bool: + """Return if the entity should be visible when first added.""" + return self.coordinator.data.device_type == 2 + + @property + def available(self) -> bool: + """Set availability if status is available.""" + return super().available and self.coordinator.data.status is not None + + @property + def is_on(self) -> bool | None: + """Return if the compressor is currently active.""" + if self.coordinator.data.status is None: + return None + return bool( + self.coordinator.data.status.main_controller_status & STATUS_BIT_COMPRESSOR + ) diff --git a/custom_components/blanco_unit/translations/en.json b/custom_components/blanco_unit/translations/en.json index cced730..25916c3 100644 --- a/custom_components/blanco_unit/translations/en.json +++ b/custom_components/blanco_unit/translations/en.json @@ -95,6 +95,12 @@ }, "cloud_connection": { "name": "Cloud Connection" + }, + "heater_active": { + "name": "Heater Active" + }, + "compressor_active": { + "name": "Compressor Active" } }, "button": { From 5f7762db6539a33ce1476b8e1dba0d79cd015d9b Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Tue, 27 Jan 2026 23:22:53 +0100 Subject: [PATCH 4/9] fix: Rename cooling_temp to Compressor Temperature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The temp_comp field measures the compressor/condenser temperature (hot side), not the cold water compartment. It idles at ~32-34°C and spikes to ~52-55°C when the compressor runs. Changed icon from snowflake-thermometer to heat-wave to reflect this. --- custom_components/blanco_unit/sensor.py | 8 ++++++-- custom_components/blanco_unit/translations/en.json | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/custom_components/blanco_unit/sensor.py b/custom_components/blanco_unit/sensor.py index 35c5236..b5944b9 100644 --- a/custom_components/blanco_unit/sensor.py +++ b/custom_components/blanco_unit/sensor.py @@ -248,14 +248,18 @@ def native_value(self) -> int | None: class CoolingTempSensor(BlancoUnitBaseEntity, SensorEntity): - """Sensor for cooling compartment temperature (CHOICE.All only).""" + """Sensor for compressor temperature (CHOICE.All only). + + Measures the compressor/condenser temperature (hot side of cooling system). + Idles at ~32-34°C, spikes to ~52-55°C when compressor is running. + """ _attr_unique_id = "cooling_temp" _attr_translation_key = _attr_unique_id _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT - _attr_icon = "mdi:snowflake-thermometer" + _attr_icon = "mdi:heat-wave" @property def entity_registry_visible_default(self) -> bool: diff --git a/custom_components/blanco_unit/translations/en.json b/custom_components/blanco_unit/translations/en.json index 25916c3..bdfc859 100644 --- a/custom_components/blanco_unit/translations/en.json +++ b/custom_components/blanco_unit/translations/en.json @@ -185,7 +185,7 @@ "name": "Boiler Temperature 2" }, "cooling_temp": { - "name": "Cooling Compartment Temperature" + "name": "Compressor Temperature" }, "main_controller_status": { "name": "Main Controller Status" From db760eb179dbbb8e6d9a10e3728c2c488ff21b4c Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:07:37 +0100 Subject: [PATCH 5/9] update snapshot --- .coverage | Bin 53248 -> 53248 bytes tests/snapshots/test_select.ambr | 137 ++++++++ tests/snapshots/test_sensor.ambr | 521 +++++++++++++++++++++++++++++-- tests/test_select.py | 2 +- tests/test_sensor.py | 2 +- 5 files changed, 637 insertions(+), 25 deletions(-) diff --git a/.coverage b/.coverage index 62056c47250fa25a6cf71d69d8e0a685298d4400..8f78d6aff7525d0fc2a0edf67251ac1ff86c4497 100644 GIT binary patch literal 53248 zcmeI4du$ZP8NhdM_a3|V7$O1}%OT!YO^m_#Y_%#%Q6V(YiZ+Tv15JfPj=l5x*!XVu ze7old#F!(MP!mOI`cQ=$9_l}-QW>OBmn4FK)T*RZGK$ig&;XY3T1?6f?NEukc138kN^@u z0!ZNhn?PY{$RDk#VK?-faw4smIXS81>@fV`_ZKf*wpd!W@LLZpmh3cXrYONNXO6T` z%IQ5)R>?^nYFd$0tzAvZrmCeRbG71>&KpWQUFaYUmO5In$*ND!;%Yn8GL;lGk8#~52|)g2K9i-L12t^HraoEC$?BSV$=J``H`nRQioI3c=^&>)YS+qZYFAz< zcYdQ(so7|mq3MkhH3B@~-k>jf|5TO%uT*-Ct~7k!ipzOZx31&R`SBJALO;4A;E&eT zu^YQAN1=G)3Dq>pCqk9el#?fM9hyJ6-_TJ~?Id&*rPGq$VGS8tTe3R`ym{i7i8(Oj z935zqUMdKhexcq*l?5H7L|RW&`xlY*y;w|M9>(n+A;=l#)X)7XMNBIvwC-`*-CXi8-W zT(uBdj*ha!FCx6%LzSiTK-EfE%&5rPeot5fy%YR9!fP>#bJ0q zq8o+^%PL*AjBhxpQvGV#ptF!sFC}z6t;m|aHb9Lf2FO$m$5S#H z-;@O=wAGBR5kt_H4G0FAYX}o{1Z>r7nxW^Y;IP|*(*TVM4!JAXenJ5Vek$m0N4KkTO4AKfO&Zi}(Ot6>c@v&3 z0fN^FJhOpy&%+X`22VzbmmMu10K;_`<;#WwX7~9)Y>#_lVdCgk;kho<(R}VYT2i+j z1tXi*z|MQ!ls3sCX z0!RP}AOR$R1Q@2DFZ8>(KdRpUlU}}HxG256EsMA=xU~NdyDYf8{|~v`(bfC^pvz{L z_y6Jmzkzkp!>-={3*CG{cT>K-{|~I;3q3B1ov{D+yX!p__2G{$eROHti+#@?yzBVEgIoXIHorS_V*aL6myYPhG5yRh?>}4H74fn{ zArTP^n|JgVj}<>t?~#3^}vaOL5F&c}oxq}&;NdF6I4EW+vCBF)r)vBxt_piKzR%q!Mi7t=zHgsuoX^Ovs`!>{^d9#CSH=jGaa z?%H>GN%7}gbJ2Kxs}?V&hF?9OJ^B96xCIC64}E$rUo1YR8DkebUX~Yxh{wABpKM~t z6>^T8CdWyUyhGk12go1DZZbr+lb6Ug$i@Q+AOR$R1dsp{Kmter2_OL^fCP{L61Wir z=(iFqAyyQLSW!4^MWK)t1%p;3idG~DRul+Wk>77cKA#nNy;j8YR>W~uZ2}yDC--v^8iZ*OqCt=bA`Ju@1Zd!=fsY1W8t^pW MXyBm%eg6Od09A6_Qvd(} delta 1650 zcma))L1@!Z7{_1I*Ctt#_oa?8M`dlYi#XR7gl?yuWJ}w1n>QV3SZjpBY(tw3=BWX} zgNQ;d>Se{F2SKYRMdnq#R1v(nc@TEcT^#5H^>stn+k1gP_}(vnzVCg%Ry)#aNA8rx zTl&?vaB2 zTYsv%dP3`JEiI>hQ#WY9x@wgGqac~_8r8LB-_7_}y-ay!xmKw=tF@}{WV5+2D4F46 zUbAKhMdJ)K89thmEL*4O5DOh|x+`wQPkZYN8s*1We3+gyEL-J@F0cB2y+)($0Ukbl zSx~H!LQ&RTB*V!fv22+u^=)3U3KB&}ShGzu-NstA;x=i^r5Hya6V^d*8))ZlBXIgf)RE079&K{sb{5NDBx{*pWnhM4G)zxpB$ z#+dwpzZzfke}7dj{=nY>o~tWw32)#W%)l9lksYe*ds0Y9SWu> Ak^lez diff --git a/tests/snapshots/test_select.ambr b/tests/snapshots/test_select.ambr index ecb9ec4..a4f1932 100644 --- a/tests/snapshots/test_select.ambr +++ b/tests/snapshots/test_select.ambr @@ -68,6 +68,143 @@ 'state': '7', }) # --- +# name: test_all_entities[select.test_blanco_unit_heating_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '60', + '61', + '62', + '63', + '64', + '65', + '66', + '67', + '68', + '69', + '70', + '71', + '72', + '73', + '74', + '75', + '76', + '77', + '78', + '79', + '80', + '81', + '82', + '83', + '84', + '85', + '86', + '87', + '88', + '89', + '90', + '91', + '92', + '93', + '94', + '95', + '96', + '97', + '98', + '99', + '100', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.test_blanco_unit_heating_temperature', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating Temperature', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heating_temperature', + 'unique_id': 'heating_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[select.test_blanco_unit_heating_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Blanco Unit Heating Temperature', + 'options': list([ + '60', + '61', + '62', + '63', + '64', + '65', + '66', + '67', + '68', + '69', + '70', + '71', + '72', + '73', + '74', + '75', + '76', + '77', + '78', + '79', + '80', + '81', + '82', + '83', + '84', + '85', + '86', + '87', + '88', + '89', + '90', + '91', + '92', + '93', + '94', + '95', + '96', + '97', + '98', + '99', + '100', + ]), + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'select.test_blanco_unit_heating_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_all_entities[select.test_blanco_unit_water_hardness_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/snapshots/test_sensor.ambr b/tests/snapshots/test_sensor.ambr index 6d08e4c..06686f1 100644 --- a/tests/snapshots/test_sensor.ambr +++ b/tests/snapshots/test_sensor.ambr @@ -48,6 +48,169 @@ 'state': 'AA:BB:CC:DD:EE:FF', }) # --- +# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_1', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:thermometer-water', + 'original_name': 'Boiler Temperature 1', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'boiler_temp_1', + 'unique_id': 'boiler_temp_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Blanco Unit Boiler Temperature 1', + 'icon': 'mdi:thermometer-water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_2', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:thermometer-water', + 'original_name': 'Boiler Temperature 2', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'boiler_temp_2', + 'unique_id': 'boiler_temp_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Blanco Unit Boiler Temperature 2', + 'icon': 'mdi:thermometer-water', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_classic_carbonation_ratio-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_classic_carbonation_ratio', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:gas-cylinder', + 'original_name': 'Classic Carbonation Ratio', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'classic_carbonation_ratio', + 'unique_id': 'classic_carbonation_ratio', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_classic_carbonation_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Classic Carbonation Ratio', + 'icon': 'mdi:gas-cylinder', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_classic_carbonation_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_all_entities[sensor.test_blanco_unit_cleaning_mode_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -199,6 +362,161 @@ 'state': '1.0.0', }) # --- +# name: test_all_entities[sensor.test_blanco_unit_connection_controller_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_connection_controller_status', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:chip', + 'original_name': 'Connection Controller Status', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'conn_controller_status', + 'unique_id': 'conn_controller_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_connection_controller_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Connection Controller Status', + 'icon': 'mdi:chip', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_connection_controller_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_cooling_compartment_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_blanco_unit_cooling_compartment_temperature', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:snowflake-thermometer', + 'original_name': 'Cooling Compartment Temperature', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cooling_temp', + 'unique_id': 'cooling_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_cooling_compartment_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Blanco Unit Cooling Compartment Temperature', + 'icon': 'mdi:snowflake-thermometer', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_cooling_compartment_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_id-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_device_id', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:information', + 'original_name': 'Device ID', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'device_id', + 'unique_id': 'device_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_id-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Device ID', + 'icon': 'mdi:information', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_device_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'test_device_id', + }) +# --- # name: test_all_entities[sensor.test_blanco_unit_device_name-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -551,6 +869,114 @@ 'state': 'AA:BB:CC:DD:EE:00', }) # --- +# name: test_all_entities[sensor.test_blanco_unit_heating_setpoint-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_heating_setpoint', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:thermometer-high', + 'original_name': 'Heating Setpoint', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heating_setpoint', + 'unique_id': 'heating_setpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_heating_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Blanco Unit Heating Setpoint', + 'icon': 'mdi:thermometer-high', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_heating_setpoint', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_hot_water_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_hot_water_calibration', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:water-thermometer', + 'original_name': 'Hot Water Calibration', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'hot_water_calibration', + 'unique_id': 'hot_water_calibration', + 'unit_of_measurement': 'mL', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_hot_water_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume', + 'friendly_name': 'Test Blanco Unit Hot Water Calibration', + 'icon': 'mdi:water-thermometer', + 'unit_of_measurement': 'mL', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_hot_water_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_all_entities[sensor.test_blanco_unit_ip_address-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -649,7 +1075,7 @@ 'state': '1.0.0', }) # --- -# name: test_all_entities[sensor.test_blanco_unit_network_gateway-entry] +# name: test_all_entities[sensor.test_blanco_unit_main_controller_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -662,9 +1088,9 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_network_gateway', + 'entity_id': 'sensor.test_blanco_unit_main_controller_status', 'has_entity_name': True, - 'hidden_by': None, + 'hidden_by': , 'icon': None, 'id': , 'labels': set({ @@ -673,32 +1099,81 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:router-network', - 'original_name': 'Network Gateway', + 'original_icon': 'mdi:chip', + 'original_name': 'Main Controller Status', 'platform': 'blanco_unit', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'gateway', - 'unique_id': 'gateway', + 'translation_key': 'main_controller_status', + 'unique_id': 'main_controller_status', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.test_blanco_unit_network_gateway-state] +# name: test_all_entities[sensor.test_blanco_unit_main_controller_status-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Network Gateway', - 'icon': 'mdi:router-network', + 'friendly_name': 'Test Blanco Unit Main Controller Status', + 'icon': 'mdi:chip', }), 'context': , - 'entity_id': 'sensor.test_blanco_unit_network_gateway', + 'entity_id': 'sensor.test_blanco_unit_main_controller_status', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '192.168.1.1', + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_medium_carbonation_ratio-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_medium_carbonation_ratio', + 'has_entity_name': True, + 'hidden_by': , + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:gas-cylinder', + 'original_name': 'Medium Carbonation Ratio', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'medium_carbonation_ratio', + 'unique_id': 'medium_carbonation_ratio', + 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.test_blanco_unit_none-entry] +# name: test_all_entities[sensor.test_blanco_unit_medium_carbonation_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Medium Carbonation Ratio', + 'icon': 'mdi:gas-cylinder', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_medium_carbonation_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_network_gateway-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -711,7 +1186,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_none', + 'entity_id': 'sensor.test_blanco_unit_network_gateway', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -722,29 +1197,29 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:information', - 'original_name': None, + 'original_icon': 'mdi:router-network', + 'original_name': 'Network Gateway', 'platform': 'blanco_unit', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'device_id', - 'unique_id': 'device_id', + 'translation_key': 'gateway', + 'unique_id': 'gateway', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.test_blanco_unit_none-state] +# name: test_all_entities[sensor.test_blanco_unit_network_gateway-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit None', - 'icon': 'mdi:information', + 'friendly_name': 'Test Blanco Unit Network Gateway', + 'icon': 'mdi:router-network', }), 'context': , - 'entity_id': 'sensor.test_blanco_unit_none', + 'entity_id': 'sensor.test_blanco_unit_network_gateway', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'test_device_id', + 'state': '192.168.1.1', }) # --- # name: test_all_entities[sensor.test_blanco_unit_post_flush_quantity-entry] diff --git a/tests/test_select.py b/tests/test_select.py index a6b9ae1..d6bba58 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -98,7 +98,7 @@ def mock_add_entities(entities): await async_setup_entry(hass, mock_config_entry, mock_add_entities) # Verify both select entities were added - assert len(entities_added) == 2 + assert len(entities_added) == 3 # Verify entity types entity_types = [type(entity).__name__ for entity in entities_added] diff --git a/tests/test_sensor.py b/tests/test_sensor.py index a864339..7a28004 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -157,7 +157,7 @@ def mock_add_entities(entities): await async_setup_entry(hass, mock_config_entry, mock_add_entities) # Verify all 24 sensors were added - assert len(entities_added) == 24 + assert len(entities_added) == 33 # Verify sensor types sensor_types = [type(entity).__name__ for entity in entities_added] From a204127e8cfa76064edf457ed17828ea3d9d26a1 Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:24:54 +0100 Subject: [PATCH 6/9] hide specific select/sensor depending on dev_type --- .coverage | Bin 53248 -> 53248 bytes custom_components/blanco_unit/select.py | 14 +- custom_components/blanco_unit/sensor.py | 66 +----- tests/snapshots/test_select.ambr | 137 ------------ tests/snapshots/test_sensor.ambr | 277 +----------------------- tests/test_select.py | 22 +- tests/test_sensor.py | 24 +- 7 files changed, 61 insertions(+), 479 deletions(-) diff --git a/.coverage b/.coverage index 8f78d6aff7525d0fc2a0edf67251ac1ff86c4497..fcb6e2c6bb41f0e1574ef6b7eaec31fd6d686586 100644 GIT binary patch delta 186 zcmZozz}&Eac>`O6!c7MLpZq8I{rH{v4fvV(?(<#d+rzh(ZxP=FzDhoB-kX~R1vc|e zzS3({pToz>$k`ad$JID@{l2~X|9-YkxPR;B_Ofr+&fU9pGuk>~zu|`a5;yXB-t;!^ zHDve}ZT)+#?1uYMH}luNSvz&_RwkIj>g;r&#^`Ti8$g=*Zh#cd-Me)&!l>$Oc2Qmy aMot~xU+1pR{aaR+y5Ep7VRLt1sUrZoV_SLv delta 191 zcmZozz}&Eac>`O6!W9PopZq8I{rH{v4fvV(?(<#d+rzh(ZxP=FzDhne-Yc601=jIS zzS(P3U&zPG$k~|0$JID@eeL~w_x~}ziMIZI?cBXvH>0hqv)SKlzjmrFdo75%7o}aD zy|&@+wR3d{@wHnwZNJ8K1Ee-R|LvQ#b8CV0&FBMQ{Ru#k?PcHKrh!$+it@5Ba_aE@ VI(NPOFI!nz>i(_io4fl;9RaUGUp4>$ diff --git a/custom_components/blanco_unit/select.py b/custom_components/blanco_unit/select.py index 1b93898..77da7f3 100644 --- a/custom_components/blanco_unit/select.py +++ b/custom_components/blanco_unit/select.py @@ -19,13 +19,13 @@ async def async_setup_entry( """Set up the Selectors for temperature and water hardness.""" coordinator: BlancoUnitCoordinator = config_entry.runtime_data - async_add_entities( - [ - TemperatureSelect(coordinator), - HeatingTemperatureSelect(coordinator), - WaterHardnessSelect(coordinator), - ] - ) + entities = [ + TemperatureSelect(coordinator), + WaterHardnessSelect(coordinator), + ] + if coordinator.data.device_type == 2: + entities.append(HeatingTemperatureSelect(coordinator)) + async_add_entities(entities) class TemperatureSelect(BlancoUnitBaseEntity, SelectEntity): diff --git a/custom_components/blanco_unit/sensor.py b/custom_components/blanco_unit/sensor.py index b5944b9..76a398b 100644 --- a/custom_components/blanco_unit/sensor.py +++ b/custom_components/blanco_unit/sensor.py @@ -28,20 +28,13 @@ async def async_setup_entry( """Set up the sensors for Blanco Unit.""" coordinator: BlancoUnitCoordinator = config_entry.runtime_data - async_add_entities( - [ + entities = [ # Status sensors FilterRemainingSensor(coordinator), CO2RemainingSensor(coordinator), TapStateSensor(coordinator), CleanModeStateSensor(coordinator), ErrorBitsSensor(coordinator), - # CHOICE.All status sensors - BoilerTemp1Sensor(coordinator), - BoilerTemp2Sensor(coordinator), - CoolingTempSensor(coordinator), - MainControllerStatusSensor(coordinator), - ConnControllerStatusSensor(coordinator), # Settings sensors FilterLifetimeSensor(coordinator), PostFlushQuantitySensor(coordinator), @@ -71,7 +64,17 @@ async def async_setup_entry( GatewayMacSensor(coordinator), SubnetSensor(coordinator), ] - ) + # CHOICE.All status sensors + entitiesExtended = [ + BoilerTemp1Sensor(coordinator), + BoilerTemp2Sensor(coordinator), + CoolingTempSensor(coordinator), + MainControllerStatusSensor(coordinator), + ConnControllerStatusSensor(coordinator), + ] + if coordinator.data.device_type == 2: + entities.extend(entitiesExtended) + async_add_entities(entities) # ------------------------------- @@ -201,11 +204,6 @@ class BoilerTemp1Sensor(BlancoUnitBaseEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:thermometer-water" - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if status is available.""" @@ -229,11 +227,6 @@ class BoilerTemp2Sensor(BlancoUnitBaseEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:thermometer-water" - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if status is available.""" @@ -261,11 +254,6 @@ class CoolingTempSensor(BlancoUnitBaseEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon = "mdi:heat-wave" - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if status is available.""" @@ -287,11 +275,6 @@ class MainControllerStatusSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:chip" _attr_entity_category = EntityCategory.DIAGNOSTIC - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if status is available.""" @@ -313,11 +296,6 @@ class ConnControllerStatusSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:chip" _attr_entity_category = EntityCategory.DIAGNOSTIC - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if status is available.""" @@ -397,11 +375,6 @@ class HeatingSetpointSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:thermometer-high" _attr_entity_category = EntityCategory.DIAGNOSTIC - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if settings are available.""" @@ -425,11 +398,6 @@ class HotWaterCalibrationSensor(BlancoUnitBaseEntity, SensorEntity): _attr_native_unit_of_measurement = "mL" _attr_entity_category = EntityCategory.DIAGNOSTIC - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if settings are available.""" @@ -451,11 +419,6 @@ class MediumCarbonationRatioSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:gas-cylinder" _attr_entity_category = EntityCategory.DIAGNOSTIC - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if settings are available.""" @@ -477,11 +440,6 @@ class ClassicCarbonationRatioSensor(BlancoUnitBaseEntity, SensorEntity): _attr_icon = "mdi:gas-cylinder" _attr_entity_category = EntityCategory.DIAGNOSTIC - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if settings are available.""" diff --git a/tests/snapshots/test_select.ambr b/tests/snapshots/test_select.ambr index a4f1932..ecb9ec4 100644 --- a/tests/snapshots/test_select.ambr +++ b/tests/snapshots/test_select.ambr @@ -68,143 +68,6 @@ 'state': '7', }) # --- -# name: test_all_entities[select.test_blanco_unit_heating_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '60', - '61', - '62', - '63', - '64', - '65', - '66', - '67', - '68', - '69', - '70', - '71', - '72', - '73', - '74', - '75', - '76', - '77', - '78', - '79', - '80', - '81', - '82', - '83', - '84', - '85', - '86', - '87', - '88', - '89', - '90', - '91', - '92', - '93', - '94', - '95', - '96', - '97', - '98', - '99', - '100', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.test_blanco_unit_heating_temperature', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Heating Temperature', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'heating_temperature', - 'unique_id': 'heating_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[select.test_blanco_unit_heating_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Test Blanco Unit Heating Temperature', - 'options': list([ - '60', - '61', - '62', - '63', - '64', - '65', - '66', - '67', - '68', - '69', - '70', - '71', - '72', - '73', - '74', - '75', - '76', - '77', - '78', - '79', - '80', - '81', - '82', - '83', - '84', - '85', - '86', - '87', - '88', - '89', - '90', - '91', - '92', - '93', - '94', - '95', - '96', - '97', - '98', - '99', - '100', - ]), - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'select.test_blanco_unit_heating_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- # name: test_all_entities[select.test_blanco_unit_water_hardness_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/snapshots/test_sensor.ambr b/tests/snapshots/test_sensor.ambr index 06686f1..2af6d8b 100644 --- a/tests/snapshots/test_sensor.ambr +++ b/tests/snapshots/test_sensor.ambr @@ -48,120 +48,6 @@ 'state': 'AA:BB:CC:DD:EE:FF', }) # --- -# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_1', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': 'mdi:thermometer-water', - 'original_name': 'Boiler Temperature 1', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'boiler_temp_1', - 'unique_id': 'boiler_temp_1', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Test Blanco Unit Boiler Temperature 1', - 'icon': 'mdi:thermometer-water', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_2', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': 'mdi:thermometer-water', - 'original_name': 'Boiler Temperature 2', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'boiler_temp_2', - 'unique_id': 'boiler_temp_2', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_boiler_temperature_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Test Blanco Unit Boiler Temperature 2', - 'icon': 'mdi:thermometer-water', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_boiler_temperature_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- # name: test_all_entities[sensor.test_blanco_unit_classic_carbonation_ratio-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -177,7 +63,7 @@ 'entity_category': , 'entity_id': 'sensor.test_blanco_unit_classic_carbonation_ratio', 'has_entity_name': True, - 'hidden_by': , + 'hidden_by': None, 'icon': None, 'id': , 'labels': set({ @@ -362,112 +248,6 @@ 'state': '1.0.0', }) # --- -# name: test_all_entities[sensor.test_blanco_unit_connection_controller_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_connection_controller_status', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:chip', - 'original_name': 'Connection Controller Status', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'conn_controller_status', - 'unique_id': 'conn_controller_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_connection_controller_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Connection Controller Status', - 'icon': 'mdi:chip', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_connection_controller_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_cooling_compartment_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_blanco_unit_cooling_compartment_temperature', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': 'mdi:snowflake-thermometer', - 'original_name': 'Cooling Compartment Temperature', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'cooling_temp', - 'unique_id': 'cooling_temp', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_cooling_compartment_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Test Blanco Unit Cooling Compartment Temperature', - 'icon': 'mdi:snowflake-thermometer', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_cooling_compartment_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- # name: test_all_entities[sensor.test_blanco_unit_device_id-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -884,7 +664,7 @@ 'entity_category': , 'entity_id': 'sensor.test_blanco_unit_heating_setpoint', 'has_entity_name': True, - 'hidden_by': , + 'hidden_by': None, 'icon': None, 'id': , 'labels': set({ @@ -938,7 +718,7 @@ 'entity_category': , 'entity_id': 'sensor.test_blanco_unit_hot_water_calibration', 'has_entity_name': True, - 'hidden_by': , + 'hidden_by': None, 'icon': None, 'id': , 'labels': set({ @@ -1075,55 +855,6 @@ 'state': '1.0.0', }) # --- -# name: test_all_entities[sensor.test_blanco_unit_main_controller_status-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_main_controller_status', - 'has_entity_name': True, - 'hidden_by': , - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:chip', - 'original_name': 'Main Controller Status', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'main_controller_status', - 'unique_id': 'main_controller_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_main_controller_status-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Main Controller Status', - 'icon': 'mdi:chip', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_main_controller_status', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- # name: test_all_entities[sensor.test_blanco_unit_medium_carbonation_ratio-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1139,7 +870,7 @@ 'entity_category': , 'entity_id': 'sensor.test_blanco_unit_medium_carbonation_ratio', 'has_entity_name': True, - 'hidden_by': , + 'hidden_by': None, 'icon': None, 'id': , 'labels': set({ diff --git a/tests/test_select.py b/tests/test_select.py index d6bba58..a0873d4 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -85,10 +85,22 @@ async def test_all_entities( ) +@pytest.mark.parametrize( + ("device_type", "expected_count"), + [ + (1, 2), + (2, 3), + ], +) async def test_async_setup_entry( - hass: HomeAssistant, mock_config_entry, mock_coordinator + hass: HomeAssistant, + mock_config_entry, + mock_coordinator, + device_type, + expected_count, ) -> None: - """Test async_setup_entry creates all select entities.""" + """Test async_setup_entry creates correct select entities per device type.""" + mock_coordinator.data.device_type = device_type mock_config_entry.runtime_data = mock_coordinator entities_added = [] @@ -97,13 +109,13 @@ def mock_add_entities(entities): await async_setup_entry(hass, mock_config_entry, mock_add_entities) - # Verify both select entities were added - assert len(entities_added) == 3 + assert len(entities_added) == expected_count - # Verify entity types entity_types = [type(entity).__name__ for entity in entities_added] assert "TemperatureSelect" in entity_types assert "WaterHardnessSelect" in entity_types + if device_type == 2: + assert "HeatingTemperatureSelect" in entity_types async def test_temperature_select(mock_coordinator) -> None: diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 7a28004..a21e860 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -144,10 +144,22 @@ async def test_all_entities( ) +@pytest.mark.parametrize( + ("device_type", "expected_count"), + [ + (1, 28), + (2, 33), + ], +) async def test_async_setup_entry( - hass: HomeAssistant, mock_config_entry, mock_coordinator + hass: HomeAssistant, + mock_config_entry, + mock_coordinator, + device_type, + expected_count, ) -> None: - """Test async_setup_entry creates all sensors.""" + """Test async_setup_entry creates correct sensors.""" + mock_coordinator.data.device_type = device_type mock_config_entry.runtime_data = mock_coordinator entities_added = [] @@ -157,7 +169,7 @@ def mock_add_entities(entities): await async_setup_entry(hass, mock_config_entry, mock_add_entities) # Verify all 24 sensors were added - assert len(entities_added) == 33 + assert len(entities_added) == expected_count # Verify sensor types sensor_types = [type(entity).__name__ for entity in entities_added] @@ -184,6 +196,12 @@ def mock_add_entities(entities): assert "GatewaySensor" in sensor_types assert "GatewayMacSensor" in sensor_types assert "SubnetSensor" in sensor_types + if device_type == 2: + assert "BoilerTemp1Sensor" in sensor_types + assert "BoilerTemp2Sensor" in sensor_types + assert "CoolingTempSensor" in sensor_types + assert "MainControllerStatusSensor" in sensor_types + assert "ConnControllerStatusSensor" in sensor_types async def test_filter_remaining_sensor(mock_coordinator) -> None: From 99c00798ec7e616617bd85c9a63ff91a5972b73a Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:39:30 +0100 Subject: [PATCH 7/9] update binary sensor test, only add binary sensors depending on dev_type --- .coverage | Bin 53248 -> 53248 bytes .../blanco_unit/binary_sensor.py | 29 +- tests/snapshots/test_select.ambr | 284 +- tests/snapshots/test_sensor.ambr | 2816 ++++++++--------- tests/test_binary_sensor.py | 21 +- tests/test_select.py | 538 ++-- tests/test_sensor.py | 1248 ++++---- 7 files changed, 2472 insertions(+), 2464 deletions(-) diff --git a/.coverage b/.coverage index fcb6e2c6bb41f0e1574ef6b7eaec31fd6d686586..977de2ddae74dd34b529f75df0762ee4b4c08f4e 100644 GIT binary patch delta 164 zcmZozz}&Eac>`O6!X*a&pZq8I{rH{v4fvV(?(<#d+se0$Z#G{iUlAV*@1@Ox0&93D z-|UU>koa@#z&(i@(##3_w{E_Fi}Cl`seiAXx)*gbT00^C?VGi8_io+1z3dy1PR(bm z&Q8yN+W-`~f9qznHJAreF}bqOSx$zNg^|;Wv&;I~@51za-Q9EN)>>QtE-Pcp=P~$j KZ1d$lT}J?=drhGL delta 157 zcmZozz}&Eac>`O6!c7MLpZq8I{rH{v4fvV(?(<#d+rzh(ZxP=FzDhoB-kX~R1vc|e zzSJAxaU-ASO>g5~LxykB*1y-vZnz(HGk@)ywNv+QW%_pQ+`U^jqphp6)9(Z6Z( None: """Set up the binary sensors.""" coordinator: BlancoUnitCoordinator = config_entry.runtime_data - async_add_entities( - [ + entities = [ ConnectionBinarySensor(coordinator), WaterDispensingBinarySensor(coordinator), FirmwareUpdateBinarySensor(coordinator), CloudConnectBinarySensor(coordinator), - # CHOICE.All status binary sensors (decoded from main_controller_status) - HeaterActiveBinarySensor(coordinator), - CompressorActiveBinarySensor(coordinator), - ] - ) + ] + + # CHOICE.All binary sensors (decoded from main_controller_status) + entitiesExtended = [ + HeaterActiveBinarySensor(coordinator), + CompressorActiveBinarySensor(coordinator), + ] + if coordinator.data.device_type == 2: + entities.extend(entitiesExtended) + async_add_entities(entities) class ConnectionBinarySensor(BlancoUnitBaseEntity, BinarySensorEntity): @@ -143,11 +146,6 @@ class HeaterActiveBinarySensor(BlancoUnitBaseEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.RUNNING _attr_icon = "mdi:fire" - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if status is available.""" @@ -176,11 +174,6 @@ class CompressorActiveBinarySensor(BlancoUnitBaseEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.RUNNING _attr_icon = "mdi:snowflake" - @property - def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added.""" - return self.coordinator.data.device_type == 2 - @property def available(self) -> bool: """Set availability if status is available.""" diff --git a/tests/snapshots/test_select.ambr b/tests/snapshots/test_select.ambr index ecb9ec4..ad7eb3d 100644 --- a/tests/snapshots/test_select.ambr +++ b/tests/snapshots/test_select.ambr @@ -1,142 +1,142 @@ -# serializer version: 1 -# name: test_all_entities[select.test_blanco_unit_cooling_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '4', - '5', - '6', - '7', - '8', - '9', - '10', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.test_blanco_unit_cooling_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Cooling Temperature', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'temperature', - 'unique_id': 'temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[select.test_blanco_unit_cooling_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Test Blanco Unit Cooling Temperature', - 'options': list([ - '4', - '5', - '6', - '7', - '8', - '9', - '10', - ]), - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'select.test_blanco_unit_cooling_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7', - }) -# --- -# name: test_all_entities[select.test_blanco_unit_water_hardness_level-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': , - 'entity_id': 'select.test_blanco_unit_water_hardness_level', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:water-opacity', - 'original_name': 'Water Hardness Level', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'water_hardness', - 'unique_id': 'water_hardness', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[select.test_blanco_unit_water_hardness_level-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Water Hardness Level', - 'icon': 'mdi:water-opacity', - 'options': list([ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - ]), - }), - 'context': , - 'entity_id': 'select.test_blanco_unit_water_hardness_level', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '5', - }) -# --- +# serializer version: 1 +# name: test_all_entities[select.test_blanco_unit_cooling_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '4', + '5', + '6', + '7', + '8', + '9', + '10', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.test_blanco_unit_cooling_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cooling Temperature', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[select.test_blanco_unit_cooling_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Blanco Unit Cooling Temperature', + 'options': list([ + '4', + '5', + '6', + '7', + '8', + '9', + '10', + ]), + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'select.test_blanco_unit_cooling_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_all_entities[select.test_blanco_unit_water_hardness_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.test_blanco_unit_water_hardness_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:water-opacity', + 'original_name': 'Water Hardness Level', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_hardness', + 'unique_id': 'water_hardness', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[select.test_blanco_unit_water_hardness_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Water Hardness Level', + 'icon': 'mdi:water-opacity', + 'options': list([ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + ]), + }), + 'context': , + 'entity_id': 'select.test_blanco_unit_water_hardness_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- diff --git a/tests/snapshots/test_sensor.ambr b/tests/snapshots/test_sensor.ambr index 2af6d8b..ab88d38 100644 --- a/tests/snapshots/test_sensor.ambr +++ b/tests/snapshots/test_sensor.ambr @@ -1,1408 +1,1408 @@ -# serializer version: 1 -# name: test_all_entities[sensor.test_blanco_unit_bluetooth_mac_address-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_bluetooth_mac_address', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:bluetooth', - 'original_name': 'Bluetooth MAC Address', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'ble_mac', - 'unique_id': 'ble_mac', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_bluetooth_mac_address-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Bluetooth MAC Address', - 'icon': 'mdi:bluetooth', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_bluetooth_mac_address', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'AA:BB:CC:DD:EE:FF', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_classic_carbonation_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_classic_carbonation_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:gas-cylinder', - 'original_name': 'Classic Carbonation Ratio', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'classic_carbonation_ratio', - 'unique_id': 'classic_carbonation_ratio', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_classic_carbonation_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Classic Carbonation Ratio', - 'icon': 'mdi:gas-cylinder', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_classic_carbonation_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_cleaning_mode_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_cleaning_mode_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:spray', - 'original_name': 'Cleaning Mode State', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'clean_mode_state', - 'unique_id': 'clean_mode_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_cleaning_mode_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Cleaning Mode State', - 'icon': 'mdi:spray', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_cleaning_mode_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_co2_cylinder_remaining-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_blanco_unit_co2_cylinder_remaining', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'CO2 Cylinder Remaining', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'co2_remaining', - 'unique_id': 'co2_remaining', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_co2_cylinder_remaining-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'Test Blanco Unit CO2 Cylinder Remaining', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_co2_cylinder_remaining', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_communication_controller_firmware-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_communication_controller_firmware', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:chip', - 'original_name': 'Communication Controller Firmware', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'firmware_comm', - 'unique_id': 'firmware_comm', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_communication_controller_firmware-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Communication Controller Firmware', - 'icon': 'mdi:chip', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_communication_controller_firmware', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.0.0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_device_id-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_device_id', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:information', - 'original_name': 'Device ID', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'device_id', - 'unique_id': 'device_id', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_device_id-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Device ID', - 'icon': 'mdi:information', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_device_id', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'test_device_id', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_device_name-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_device_name', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:label', - 'original_name': 'Device Name', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'device_name', - 'unique_id': 'device_name', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_device_name-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Device Name', - 'icon': 'mdi:label', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_device_name', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'Test Device', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_device_type-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_device_type', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:information', - 'original_name': 'Device Type', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'device_type', - 'unique_id': 'device_type', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_device_type-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Device Type', - 'icon': 'mdi:information', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_device_type', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_electronic_controller_firmware-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_electronic_controller_firmware', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:chip', - 'original_name': 'Electronic Controller Firmware', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'firmware_elec', - 'unique_id': 'firmware_elec', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_electronic_controller_firmware-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Electronic Controller Firmware', - 'icon': 'mdi:chip', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_electronic_controller_firmware', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.0.0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_error_code-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_error_code', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alert-circle', - 'original_name': 'Error Code', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'error_bits', - 'unique_id': 'error_bits', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_error_code-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Error Code', - 'icon': 'mdi:alert-circle', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_error_code', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_filter_capacity_remaining-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.test_blanco_unit_filter_capacity_remaining', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Filter Capacity Remaining', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'filter_remaining', - 'unique_id': 'filter_remaining', - 'unit_of_measurement': '%', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_filter_capacity_remaining-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'Test Blanco Unit Filter Capacity Remaining', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_filter_capacity_remaining', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_filter_lifetime-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_filter_lifetime', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': 'mdi:calendar-clock', - 'original_name': 'Filter Lifetime', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'filter_lifetime', - 'unique_id': 'filter_lifetime', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_filter_lifetime-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'duration', - 'friendly_name': 'Test Blanco Unit Filter Lifetime', - 'icon': 'mdi:calendar-clock', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_filter_lifetime', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '365', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_gateway_mac_address-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_gateway_mac_address', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:router-network', - 'original_name': 'Gateway MAC Address', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'gateway_mac', - 'unique_id': 'gateway_mac', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_gateway_mac_address-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Gateway MAC Address', - 'icon': 'mdi:router-network', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_gateway_mac_address', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'AA:BB:CC:DD:EE:00', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_heating_setpoint-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_heating_setpoint', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': 'mdi:thermometer-high', - 'original_name': 'Heating Setpoint', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'heating_setpoint', - 'unique_id': 'heating_setpoint', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_heating_setpoint-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Test Blanco Unit Heating Setpoint', - 'icon': 'mdi:thermometer-high', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_heating_setpoint', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_hot_water_calibration-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_hot_water_calibration', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': 'mdi:water-thermometer', - 'original_name': 'Hot Water Calibration', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'hot_water_calibration', - 'unique_id': 'hot_water_calibration', - 'unit_of_measurement': 'mL', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_hot_water_calibration-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'volume', - 'friendly_name': 'Test Blanco Unit Hot Water Calibration', - 'icon': 'mdi:water-thermometer', - 'unit_of_measurement': 'mL', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_hot_water_calibration', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_ip_address-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_ip_address', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:ip-network', - 'original_name': 'IP Address', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'ip_address', - 'unique_id': 'ip_address', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_ip_address-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit IP Address', - 'icon': 'mdi:ip-network', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_ip_address', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '192.168.1.100', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_main_controller_firmware-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_main_controller_firmware', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:chip', - 'original_name': 'Main Controller Firmware', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'firmware_main', - 'unique_id': 'firmware_main', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_main_controller_firmware-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Main Controller Firmware', - 'icon': 'mdi:chip', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_main_controller_firmware', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.0.0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_medium_carbonation_ratio-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_medium_carbonation_ratio', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:gas-cylinder', - 'original_name': 'Medium Carbonation Ratio', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'medium_carbonation_ratio', - 'unique_id': 'medium_carbonation_ratio', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_medium_carbonation_ratio-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Medium Carbonation Ratio', - 'icon': 'mdi:gas-cylinder', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_medium_carbonation_ratio', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_network_gateway-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_network_gateway', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:router-network', - 'original_name': 'Network Gateway', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'gateway', - 'unique_id': 'gateway', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_network_gateway-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Network Gateway', - 'icon': 'mdi:router-network', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_network_gateway', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '192.168.1.1', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_post_flush_quantity-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_post_flush_quantity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': 'mdi:water', - 'original_name': 'Post-Flush Quantity', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'post_flush_quantity', - 'unique_id': 'post_flush_quantity', - 'unit_of_measurement': 'mL', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_post_flush_quantity-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'volume', - 'friendly_name': 'Test Blanco Unit Post-Flush Quantity', - 'icon': 'mdi:water', - 'unit_of_measurement': 'mL', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_post_flush_quantity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_reset_count-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_reset_count', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:counter', - 'original_name': 'Reset Count', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'reset_count', - 'unique_id': 'reset_count', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_reset_count-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Reset Count', - 'icon': 'mdi:counter', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_reset_count', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_serial_number-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_serial_number', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:barcode', - 'original_name': 'Serial Number', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'serial_number', - 'unique_id': 'serial_number', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_serial_number-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Serial Number', - 'icon': 'mdi:barcode', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_serial_number', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '123456', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_service_code-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_service_code', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:barcode-scan', - 'original_name': 'Service Code', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'service_code', - 'unique_id': 'service_code', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_service_code-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Service Code', - 'icon': 'mdi:barcode-scan', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_service_code', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'ABCDEF', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_subnet_mask-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_subnet_mask', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:ip-network-outline', - 'original_name': 'Subnet Mask', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'subnet', - 'unique_id': 'subnet', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_subnet_mask-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Subnet Mask', - 'icon': 'mdi:ip-network-outline', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_subnet_mask', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '255.255.255.0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_tap_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_tap_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:water-pump', - 'original_name': 'Tap State', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'tap_state', - 'unique_id': 'tap_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_tap_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit Tap State', - 'icon': 'mdi:water-pump', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_tap_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_wifi_mac_address-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_wifi_mac_address', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:network', - 'original_name': 'WiFi MAC Address', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'wifi_mac', - 'unique_id': 'wifi_mac', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_wifi_mac_address-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit WiFi MAC Address', - 'icon': 'mdi:network', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_wifi_mac_address', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '11:22:33:44:55:66', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_wifi_network_name-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_wifi_network_name', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:wifi', - 'original_name': 'WiFi Network Name', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'wifi_ssid', - 'unique_id': 'wifi_ssid', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_wifi_network_name-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit WiFi Network Name', - 'icon': 'mdi:wifi', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_wifi_network_name', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'TestSSID', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_wifi_signal_strength-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_blanco_unit_wifi_signal_strength', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'WiFi Signal Strength', - 'platform': 'blanco_unit', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'wifi_signal', - 'unique_id': 'wifi_signal', - 'unit_of_measurement': 'dBm', - }) -# --- -# name: test_all_entities[sensor.test_blanco_unit_wifi_signal_strength-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'signal_strength', - 'friendly_name': 'Test Blanco Unit WiFi Signal Strength', - 'state_class': , - 'unit_of_measurement': 'dBm', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_wifi_signal_strength', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '-50', - }) -# --- +# serializer version: 1 +# name: test_all_entities[sensor.test_blanco_unit_bluetooth_mac_address-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_bluetooth_mac_address', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:bluetooth', + 'original_name': 'Bluetooth MAC Address', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ble_mac', + 'unique_id': 'ble_mac', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_bluetooth_mac_address-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Bluetooth MAC Address', + 'icon': 'mdi:bluetooth', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_bluetooth_mac_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'AA:BB:CC:DD:EE:FF', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_classic_carbonation_ratio-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_classic_carbonation_ratio', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:gas-cylinder', + 'original_name': 'Classic Carbonation Ratio', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'classic_carbonation_ratio', + 'unique_id': 'classic_carbonation_ratio', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_classic_carbonation_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Classic Carbonation Ratio', + 'icon': 'mdi:gas-cylinder', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_classic_carbonation_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_cleaning_mode_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_cleaning_mode_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:spray', + 'original_name': 'Cleaning Mode State', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'clean_mode_state', + 'unique_id': 'clean_mode_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_cleaning_mode_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Cleaning Mode State', + 'icon': 'mdi:spray', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_cleaning_mode_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_co2_cylinder_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_blanco_unit_co2_cylinder_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CO2 Cylinder Remaining', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'co2_remaining', + 'unique_id': 'co2_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_co2_cylinder_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test Blanco Unit CO2 Cylinder Remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_co2_cylinder_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_communication_controller_firmware-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_communication_controller_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:chip', + 'original_name': 'Communication Controller Firmware', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'firmware_comm', + 'unique_id': 'firmware_comm', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_communication_controller_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Communication Controller Firmware', + 'icon': 'mdi:chip', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_communication_controller_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0.0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_id-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_device_id', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:information', + 'original_name': 'Device ID', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'device_id', + 'unique_id': 'device_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_id-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Device ID', + 'icon': 'mdi:information', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_device_id', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'test_device_id', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_name-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_device_name', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:label', + 'original_name': 'Device Name', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'device_name', + 'unique_id': 'device_name', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_name-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Device Name', + 'icon': 'mdi:label', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_device_name', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Test Device', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_device_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:information', + 'original_name': 'Device Type', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'device_type', + 'unique_id': 'device_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_device_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Device Type', + 'icon': 'mdi:information', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_device_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_electronic_controller_firmware-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_electronic_controller_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:chip', + 'original_name': 'Electronic Controller Firmware', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'firmware_elec', + 'unique_id': 'firmware_elec', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_electronic_controller_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Electronic Controller Firmware', + 'icon': 'mdi:chip', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_electronic_controller_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0.0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_error_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_error_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:alert-circle', + 'original_name': 'Error Code', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'error_bits', + 'unique_id': 'error_bits', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_error_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Error Code', + 'icon': 'mdi:alert-circle', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_error_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_filter_capacity_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_blanco_unit_filter_capacity_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Filter Capacity Remaining', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_remaining', + 'unique_id': 'filter_remaining', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_filter_capacity_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test Blanco Unit Filter Capacity Remaining', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_filter_capacity_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_filter_lifetime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_filter_lifetime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:calendar-clock', + 'original_name': 'Filter Lifetime', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_lifetime', + 'unique_id': 'filter_lifetime', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_filter_lifetime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Test Blanco Unit Filter Lifetime', + 'icon': 'mdi:calendar-clock', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_filter_lifetime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '365', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_gateway_mac_address-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_gateway_mac_address', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:router-network', + 'original_name': 'Gateway MAC Address', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gateway_mac', + 'unique_id': 'gateway_mac', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_gateway_mac_address-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Gateway MAC Address', + 'icon': 'mdi:router-network', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_gateway_mac_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'AA:BB:CC:DD:EE:00', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_heating_setpoint-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_heating_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:thermometer-high', + 'original_name': 'Heating Setpoint', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heating_setpoint', + 'unique_id': 'heating_setpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_heating_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Blanco Unit Heating Setpoint', + 'icon': 'mdi:thermometer-high', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_heating_setpoint', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_hot_water_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_hot_water_calibration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:water-thermometer', + 'original_name': 'Hot Water Calibration', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'hot_water_calibration', + 'unique_id': 'hot_water_calibration', + 'unit_of_measurement': 'mL', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_hot_water_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume', + 'friendly_name': 'Test Blanco Unit Hot Water Calibration', + 'icon': 'mdi:water-thermometer', + 'unit_of_measurement': 'mL', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_hot_water_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_ip_address-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_ip_address', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ip-network', + 'original_name': 'IP Address', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ip_address', + 'unique_id': 'ip_address', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_ip_address-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit IP Address', + 'icon': 'mdi:ip-network', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_ip_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '192.168.1.100', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_main_controller_firmware-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_main_controller_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:chip', + 'original_name': 'Main Controller Firmware', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'firmware_main', + 'unique_id': 'firmware_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_main_controller_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Main Controller Firmware', + 'icon': 'mdi:chip', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_main_controller_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0.0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_medium_carbonation_ratio-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_medium_carbonation_ratio', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:gas-cylinder', + 'original_name': 'Medium Carbonation Ratio', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'medium_carbonation_ratio', + 'unique_id': 'medium_carbonation_ratio', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_medium_carbonation_ratio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Medium Carbonation Ratio', + 'icon': 'mdi:gas-cylinder', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_medium_carbonation_ratio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_network_gateway-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_network_gateway', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:router-network', + 'original_name': 'Network Gateway', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gateway', + 'unique_id': 'gateway', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_network_gateway-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Network Gateway', + 'icon': 'mdi:router-network', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_network_gateway', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '192.168.1.1', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_post_flush_quantity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_post_flush_quantity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:water', + 'original_name': 'Post-Flush Quantity', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'post_flush_quantity', + 'unique_id': 'post_flush_quantity', + 'unit_of_measurement': 'mL', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_post_flush_quantity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume', + 'friendly_name': 'Test Blanco Unit Post-Flush Quantity', + 'icon': 'mdi:water', + 'unit_of_measurement': 'mL', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_post_flush_quantity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_reset_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_reset_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:counter', + 'original_name': 'Reset Count', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_count', + 'unique_id': 'reset_count', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_reset_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Reset Count', + 'icon': 'mdi:counter', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_reset_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_serial_number-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_serial_number', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:barcode', + 'original_name': 'Serial Number', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'serial_number', + 'unique_id': 'serial_number', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_serial_number-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Serial Number', + 'icon': 'mdi:barcode', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_serial_number', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '123456', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_service_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_service_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:barcode-scan', + 'original_name': 'Service Code', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'service_code', + 'unique_id': 'service_code', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_service_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Service Code', + 'icon': 'mdi:barcode-scan', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_service_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ABCDEF', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_subnet_mask-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_subnet_mask', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ip-network-outline', + 'original_name': 'Subnet Mask', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'subnet', + 'unique_id': 'subnet', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_subnet_mask-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Subnet Mask', + 'icon': 'mdi:ip-network-outline', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_subnet_mask', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '255.255.255.0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_tap_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_tap_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:water-pump', + 'original_name': 'Tap State', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'tap_state', + 'unique_id': 'tap_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_tap_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit Tap State', + 'icon': 'mdi:water-pump', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_tap_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_wifi_mac_address-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_wifi_mac_address', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:network', + 'original_name': 'WiFi MAC Address', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_mac', + 'unique_id': 'wifi_mac', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_wifi_mac_address-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit WiFi MAC Address', + 'icon': 'mdi:network', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_wifi_mac_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11:22:33:44:55:66', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_wifi_network_name-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_wifi_network_name', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:wifi', + 'original_name': 'WiFi Network Name', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_ssid', + 'unique_id': 'wifi_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_wifi_network_name-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blanco Unit WiFi Network Name', + 'icon': 'mdi:wifi', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_wifi_network_name', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'TestSSID', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_wifi_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_blanco_unit_wifi_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'WiFi Signal Strength', + 'platform': 'blanco_unit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_signal', + 'unique_id': 'wifi_signal', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_all_entities[sensor.test_blanco_unit_wifi_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Test Blanco Unit WiFi Signal Strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.test_blanco_unit_wifi_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-50', + }) +# --- diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 6aa6a91..e492716 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -102,10 +102,22 @@ async def test_all_entities( ) +@pytest.mark.parametrize( + ("device_type", "expected_count"), + [ + (1, 4), + (2, 6), + ], +) async def test_async_setup_entry( - hass: HomeAssistant, mock_config_entry, mock_coordinator + hass: HomeAssistant, + mock_config_entry, + mock_coordinator, + device_type, + expected_count, ) -> None: - """Test async_setup_entry creates all binary sensors.""" + """Test async_setup_entry creates correct binary sensors.""" + mock_coordinator.data.device_type = device_type mock_config_entry.runtime_data = mock_coordinator entities_added = [] @@ -115,7 +127,7 @@ def mock_add_entities(entities): await async_setup_entry(hass, mock_config_entry, mock_add_entities) # Verify all 4 binary sensors were added - assert len(entities_added) == 4 + assert len(entities_added) == expected_count # Verify sensor types sensor_types = [type(entity).__name__ for entity in entities_added] @@ -123,6 +135,9 @@ def mock_add_entities(entities): assert "WaterDispensingBinarySensor" in sensor_types assert "FirmwareUpdateBinarySensor" in sensor_types assert "CloudConnectBinarySensor" in sensor_types + if device_type == 2: + assert "HeaterActiveBinarySensor" in sensor_types + assert "CompressorActiveBinarySensor" in sensor_types async def test_connection_binary_sensor(mock_coordinator) -> None: diff --git a/tests/test_select.py b/tests/test_select.py index a0873d4..0ccdd9d 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -1,269 +1,269 @@ -"""Tests for the Blanco Unit select entities.""" - -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from pytest_homeassistant_custom_component.common import ( - MockConfigEntry, - snapshot_platform, -) -from syrupy.assertion import SnapshotAssertion - -from custom_components.blanco_unit.const import CONF_MAC, CONF_NAME, CONF_PIN, DOMAIN -from custom_components.blanco_unit.data import BlancoUnitData, BlancoUnitSettings -from custom_components.blanco_unit.select import ( - TemperatureSelect, - WaterHardnessSelect, - async_setup_entry, -) -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from .conftest import setup_integration # noqa: TID251 - - -@pytest.fixture -def mock_coordinator(): - """Create a mock coordinator.""" - coordinator = MagicMock() - coordinator.data = BlancoUnitData( - connected=True, - available=True, - device_id="test_device_id", - settings=BlancoUnitSettings( - calib_still_wtr=5, - calib_soda_wtr=6, - filter_life_tm=365, - post_flush_quantity=100, - set_point_cooling=7, - wtr_hardness=5, - ), - ) - coordinator.set_temperature = AsyncMock() - coordinator.set_water_hardness = AsyncMock() - return coordinator - - -@pytest.fixture -def mock_config_entry(): - """Create a mock config entry.""" - entry = MagicMock() - entry.entry_id = "test_entry_id" - return entry - - -async def test_all_entities( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - entity_registry: er.EntityRegistry, - mock_blanco_unit_data, - mock_bluetooth_device, -) -> None: - """Test all entities.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_MAC: "AA:BB:CC:DD:EE:FF", - CONF_NAME: "Test Blanco Unit", - CONF_PIN: 12345, - }, - unique_id="AA:BB:CC:DD:EE:FF", - ) - - with ( - patch("custom_components.blanco_unit.PLATFORMS", [Platform.SELECT]), - patch( - "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", - return_value=mock_bluetooth_device, - ), - ): - await setup_integration(hass, mock_config_entry) - - await snapshot_platform( - hass, entity_registry, snapshot, mock_config_entry.entry_id - ) - - -@pytest.mark.parametrize( - ("device_type", "expected_count"), - [ - (1, 2), - (2, 3), - ], -) -async def test_async_setup_entry( - hass: HomeAssistant, - mock_config_entry, - mock_coordinator, - device_type, - expected_count, -) -> None: - """Test async_setup_entry creates correct select entities per device type.""" - mock_coordinator.data.device_type = device_type - mock_config_entry.runtime_data = mock_coordinator - entities_added = [] - - def mock_add_entities(entities): - entities_added.extend(entities) - - await async_setup_entry(hass, mock_config_entry, mock_add_entities) - - assert len(entities_added) == expected_count - - entity_types = [type(entity).__name__ for entity in entities_added] - assert "TemperatureSelect" in entity_types - assert "WaterHardnessSelect" in entity_types - if device_type == 2: - assert "HeatingTemperatureSelect" in entity_types - - -async def test_temperature_select(mock_coordinator) -> None: - """Test TemperatureSelect.""" - select = TemperatureSelect(mock_coordinator) - - assert select.available is True - assert select.current_option == "7" - assert select.unique_id == "temperature" - - -async def test_temperature_select_options(mock_coordinator) -> None: - """Test TemperatureSelect has correct options.""" - select = TemperatureSelect(mock_coordinator) - - expected_options = ["4", "5", "6", "7", "8", "9", "10"] - assert select.options == expected_options - - -async def test_temperature_select_option(mock_coordinator) -> None: - """Test TemperatureSelect async_select_option method.""" - select = TemperatureSelect(mock_coordinator) - - await select.async_select_option("8") - - mock_coordinator.set_temperature.assert_called_once_with(8) - - -async def test_temperature_select_unavailable(mock_coordinator) -> None: - """Test TemperatureSelect when settings is None.""" - mock_coordinator.data.settings = None - select = TemperatureSelect(mock_coordinator) - - assert select.available is False - assert select.current_option is None - - -async def test_water_hardness_select(mock_coordinator) -> None: - """Test WaterHardnessSelect.""" - select = WaterHardnessSelect(mock_coordinator) - - assert select.available is True - assert select.current_option == "5" - assert select.unique_id == "water_hardness" - - -async def test_water_hardness_select_options(mock_coordinator) -> None: - """Test WaterHardnessSelect has correct options.""" - select = WaterHardnessSelect(mock_coordinator) - - expected_options = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] - assert select.options == expected_options - - -async def test_water_hardness_select_option(mock_coordinator) -> None: - """Test WaterHardnessSelect async_select_option method.""" - select = WaterHardnessSelect(mock_coordinator) - - await select.async_select_option("7") - - mock_coordinator.set_water_hardness.assert_called_once_with(7) - - -async def test_water_hardness_select_unavailable(mock_coordinator) -> None: - """Test WaterHardnessSelect when settings is None.""" - mock_coordinator.data.settings = None - select = WaterHardnessSelect(mock_coordinator) - - assert select.available is False - assert select.current_option is None - - -async def test_temperature_select_all_options(mock_coordinator) -> None: - """Test TemperatureSelect can select all valid options.""" - select = TemperatureSelect(mock_coordinator) - - for temp in ["4", "5", "6", "7", "8", "9", "10"]: - await select.async_select_option(temp) - - assert mock_coordinator.set_temperature.call_count == 7 - mock_coordinator.set_temperature.assert_any_call(4) - mock_coordinator.set_temperature.assert_any_call(5) - mock_coordinator.set_temperature.assert_any_call(6) - mock_coordinator.set_temperature.assert_any_call(7) - mock_coordinator.set_temperature.assert_any_call(8) - mock_coordinator.set_temperature.assert_any_call(9) - mock_coordinator.set_temperature.assert_any_call(10) - - -async def test_water_hardness_select_all_options(mock_coordinator) -> None: - """Test WaterHardnessSelect can select all valid options.""" - select = WaterHardnessSelect(mock_coordinator) - - for hardness in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]: - await select.async_select_option(hardness) - - assert mock_coordinator.set_water_hardness.call_count == 9 - for i in range(1, 10): - mock_coordinator.set_water_hardness.assert_any_call(i) - - -async def test_select_entity_when_data_unavailable(mock_coordinator) -> None: - """Test select entity when data is unavailable.""" - mock_coordinator.data.available = False - select = TemperatureSelect(mock_coordinator) - - assert select.available is False - - -async def test_temperature_select_different_values(mock_coordinator) -> None: - """Test TemperatureSelect with different current values.""" - select = TemperatureSelect(mock_coordinator) - - # Test with different temperature values - mock_coordinator.data.settings.set_point_cooling = 4 - assert select.current_option == "4" - - mock_coordinator.data.settings.set_point_cooling = 10 - assert select.current_option == "10" - - -async def test_water_hardness_select_different_values(mock_coordinator) -> None: - """Test WaterHardnessSelect with different current values.""" - select = WaterHardnessSelect(mock_coordinator) - - # Test with different hardness values - mock_coordinator.data.settings.wtr_hardness = 1 - assert select.current_option == "1" - - mock_coordinator.data.settings.wtr_hardness = 9 - assert select.current_option == "9" - - -async def test_temperature_select_string_to_int_conversion(mock_coordinator) -> None: - """Test that TemperatureSelect converts string to int.""" - select = TemperatureSelect(mock_coordinator) - - await select.async_select_option("8") - - # Should convert to int - mock_coordinator.set_temperature.assert_called_once_with(8) - - -async def test_water_hardness_select_string_to_int_conversion(mock_coordinator) -> None: - """Test that WaterHardnessSelect converts string to int.""" - select = WaterHardnessSelect(mock_coordinator) - - await select.async_select_option("7") - - # Should convert to int - mock_coordinator.set_water_hardness.assert_called_once_with(7) +"""Tests for the Blanco Unit select entities.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from pytest_homeassistant_custom_component.common import ( + MockConfigEntry, + snapshot_platform, +) +from syrupy.assertion import SnapshotAssertion + +from custom_components.blanco_unit.const import CONF_MAC, CONF_NAME, CONF_PIN, DOMAIN +from custom_components.blanco_unit.data import BlancoUnitData, BlancoUnitSettings +from custom_components.blanco_unit.select import ( + TemperatureSelect, + WaterHardnessSelect, + async_setup_entry, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_integration # noqa: TID251 + + +@pytest.fixture +def mock_coordinator(): + """Create a mock coordinator.""" + coordinator = MagicMock() + coordinator.data = BlancoUnitData( + connected=True, + available=True, + device_id="test_device_id", + settings=BlancoUnitSettings( + calib_still_wtr=5, + calib_soda_wtr=6, + filter_life_tm=365, + post_flush_quantity=100, + set_point_cooling=7, + wtr_hardness=5, + ), + ) + coordinator.set_temperature = AsyncMock() + coordinator.set_water_hardness = AsyncMock() + return coordinator + + +@pytest.fixture +def mock_config_entry(): + """Create a mock config entry.""" + entry = MagicMock() + entry.entry_id = "test_entry_id" + return entry + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_blanco_unit_data, + mock_bluetooth_device, +) -> None: + """Test all entities.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_NAME: "Test Blanco Unit", + CONF_PIN: 12345, + }, + unique_id="AA:BB:CC:DD:EE:FF", + ) + + with ( + patch("custom_components.blanco_unit.PLATFORMS", [Platform.SELECT]), + patch( + "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", + return_value=mock_bluetooth_device, + ), + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.parametrize( + ("device_type", "expected_count"), + [ + (1, 2), + (2, 3), + ], +) +async def test_async_setup_entry( + hass: HomeAssistant, + mock_config_entry, + mock_coordinator, + device_type, + expected_count, +) -> None: + """Test async_setup_entry creates correct select entities per device type.""" + mock_coordinator.data.device_type = device_type + mock_config_entry.runtime_data = mock_coordinator + entities_added = [] + + def mock_add_entities(entities): + entities_added.extend(entities) + + await async_setup_entry(hass, mock_config_entry, mock_add_entities) + + assert len(entities_added) == expected_count + + entity_types = [type(entity).__name__ for entity in entities_added] + assert "TemperatureSelect" in entity_types + assert "WaterHardnessSelect" in entity_types + if device_type == 2: + assert "HeatingTemperatureSelect" in entity_types + + +async def test_temperature_select(mock_coordinator) -> None: + """Test TemperatureSelect.""" + select = TemperatureSelect(mock_coordinator) + + assert select.available is True + assert select.current_option == "7" + assert select.unique_id == "temperature" + + +async def test_temperature_select_options(mock_coordinator) -> None: + """Test TemperatureSelect has correct options.""" + select = TemperatureSelect(mock_coordinator) + + expected_options = ["4", "5", "6", "7", "8", "9", "10"] + assert select.options == expected_options + + +async def test_temperature_select_option(mock_coordinator) -> None: + """Test TemperatureSelect async_select_option method.""" + select = TemperatureSelect(mock_coordinator) + + await select.async_select_option("8") + + mock_coordinator.set_temperature.assert_called_once_with(8) + + +async def test_temperature_select_unavailable(mock_coordinator) -> None: + """Test TemperatureSelect when settings is None.""" + mock_coordinator.data.settings = None + select = TemperatureSelect(mock_coordinator) + + assert select.available is False + assert select.current_option is None + + +async def test_water_hardness_select(mock_coordinator) -> None: + """Test WaterHardnessSelect.""" + select = WaterHardnessSelect(mock_coordinator) + + assert select.available is True + assert select.current_option == "5" + assert select.unique_id == "water_hardness" + + +async def test_water_hardness_select_options(mock_coordinator) -> None: + """Test WaterHardnessSelect has correct options.""" + select = WaterHardnessSelect(mock_coordinator) + + expected_options = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + assert select.options == expected_options + + +async def test_water_hardness_select_option(mock_coordinator) -> None: + """Test WaterHardnessSelect async_select_option method.""" + select = WaterHardnessSelect(mock_coordinator) + + await select.async_select_option("7") + + mock_coordinator.set_water_hardness.assert_called_once_with(7) + + +async def test_water_hardness_select_unavailable(mock_coordinator) -> None: + """Test WaterHardnessSelect when settings is None.""" + mock_coordinator.data.settings = None + select = WaterHardnessSelect(mock_coordinator) + + assert select.available is False + assert select.current_option is None + + +async def test_temperature_select_all_options(mock_coordinator) -> None: + """Test TemperatureSelect can select all valid options.""" + select = TemperatureSelect(mock_coordinator) + + for temp in ["4", "5", "6", "7", "8", "9", "10"]: + await select.async_select_option(temp) + + assert mock_coordinator.set_temperature.call_count == 7 + mock_coordinator.set_temperature.assert_any_call(4) + mock_coordinator.set_temperature.assert_any_call(5) + mock_coordinator.set_temperature.assert_any_call(6) + mock_coordinator.set_temperature.assert_any_call(7) + mock_coordinator.set_temperature.assert_any_call(8) + mock_coordinator.set_temperature.assert_any_call(9) + mock_coordinator.set_temperature.assert_any_call(10) + + +async def test_water_hardness_select_all_options(mock_coordinator) -> None: + """Test WaterHardnessSelect can select all valid options.""" + select = WaterHardnessSelect(mock_coordinator) + + for hardness in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]: + await select.async_select_option(hardness) + + assert mock_coordinator.set_water_hardness.call_count == 9 + for i in range(1, 10): + mock_coordinator.set_water_hardness.assert_any_call(i) + + +async def test_select_entity_when_data_unavailable(mock_coordinator) -> None: + """Test select entity when data is unavailable.""" + mock_coordinator.data.available = False + select = TemperatureSelect(mock_coordinator) + + assert select.available is False + + +async def test_temperature_select_different_values(mock_coordinator) -> None: + """Test TemperatureSelect with different current values.""" + select = TemperatureSelect(mock_coordinator) + + # Test with different temperature values + mock_coordinator.data.settings.set_point_cooling = 4 + assert select.current_option == "4" + + mock_coordinator.data.settings.set_point_cooling = 10 + assert select.current_option == "10" + + +async def test_water_hardness_select_different_values(mock_coordinator) -> None: + """Test WaterHardnessSelect with different current values.""" + select = WaterHardnessSelect(mock_coordinator) + + # Test with different hardness values + mock_coordinator.data.settings.wtr_hardness = 1 + assert select.current_option == "1" + + mock_coordinator.data.settings.wtr_hardness = 9 + assert select.current_option == "9" + + +async def test_temperature_select_string_to_int_conversion(mock_coordinator) -> None: + """Test that TemperatureSelect converts string to int.""" + select = TemperatureSelect(mock_coordinator) + + await select.async_select_option("8") + + # Should convert to int + mock_coordinator.set_temperature.assert_called_once_with(8) + + +async def test_water_hardness_select_string_to_int_conversion(mock_coordinator) -> None: + """Test that WaterHardnessSelect converts string to int.""" + select = WaterHardnessSelect(mock_coordinator) + + await select.async_select_option("7") + + # Should convert to int + mock_coordinator.set_water_hardness.assert_called_once_with(7) diff --git a/tests/test_sensor.py b/tests/test_sensor.py index a21e860..6de3cc9 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,624 +1,624 @@ -"""Tests for the Blanco Unit sensor entities.""" - -from unittest.mock import MagicMock, patch - -import pytest -from pytest_homeassistant_custom_component.common import ( - MockConfigEntry, - snapshot_platform, -) -from syrupy.assertion import SnapshotAssertion - -from custom_components.blanco_unit.const import CONF_MAC, CONF_NAME, CONF_PIN, DOMAIN -from custom_components.blanco_unit.data import ( - BlancoUnitData, - BlancoUnitIdentity, - BlancoUnitSettings, - BlancoUnitStatus, - BlancoUnitSystemInfo, - BlancoUnitWifiInfo, -) -from custom_components.blanco_unit.sensor import ( - BLEMacSensor, - CleanModeStateSensor, - CO2RemainingSensor, - DeviceNameSensor, - DeviceTypeSensor, - ErrorBitsSensor, - FilterLifetimeSensor, - FilterRemainingSensor, - FirmwareCommSensor, - FirmwareElecSensor, - FirmwareMainSensor, - GatewayMacSensor, - GatewaySensor, - IPAddressSensor, - PostFlushQuantitySensor, - ResetCountSensor, - SerialNumberSensor, - ServiceCodeSensor, - SubnetSensor, - TapStateSensor, - WiFiMacSensor, - WiFiSignalSensor, - WiFiSSIDSensor, - async_setup_entry, -) -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from .conftest import setup_integration # noqa: TID251 - - -@pytest.fixture -def mock_coordinator(): - """Create a mock coordinator.""" - coordinator = MagicMock() - coordinator.data = BlancoUnitData( - connected=True, - available=True, - device_id="test_device_id", - device_type=1, - status=BlancoUnitStatus( - tap_state=1, - filter_rest=85, - co2_rest=90, - wtr_disp_active=True, - firm_upd_avlb=False, - set_point_cooling=7, - clean_mode_state=0, - err_bits=0, - ), - settings=BlancoUnitSettings( - calib_still_wtr=5, - calib_soda_wtr=5, - filter_life_tm=365, - post_flush_quantity=100, - set_point_cooling=7, - wtr_hardness=5, - ), - system_info=BlancoUnitSystemInfo( - sw_ver_comm_con="1.0.0", - sw_ver_elec_con="1.1.0", - sw_ver_main_con="1.2.0", - dev_name="Test Device", - reset_cnt=10, - ), - identity=BlancoUnitIdentity( - serial_no="123456", - service_code="ABCDEF", - ), - wifi_info=BlancoUnitWifiInfo( - cloud_connect=True, - ssid="TestSSID", - signal=-50, - ip="192.168.1.100", - ble_mac="AA:BB:CC:DD:EE:FF", - wifi_mac="11:22:33:44:55:66", - gateway="192.168.1.1", - gateway_mac="AA:BB:CC:DD:EE:00", - subnet="255.255.255.0", - ), - ) - return coordinator - - -@pytest.fixture -def mock_config_entry(): - """Create a mock config entry.""" - entry = MagicMock() - entry.entry_id = "test_entry_id" - return entry - - -async def test_all_entities( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - entity_registry: er.EntityRegistry, - mock_blanco_unit_data, - mock_bluetooth_device, -) -> None: - """Test all entities.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_MAC: "AA:BB:CC:DD:EE:FF", - CONF_NAME: "Test Blanco Unit", - CONF_PIN: 12345, - }, - unique_id="AA:BB:CC:DD:EE:FF", - ) - - with ( - patch("custom_components.blanco_unit.PLATFORMS", [Platform.SENSOR]), - patch( - "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", - return_value=mock_bluetooth_device, - ), - ): - await setup_integration(hass, mock_config_entry) - - await snapshot_platform( - hass, entity_registry, snapshot, mock_config_entry.entry_id - ) - - -@pytest.mark.parametrize( - ("device_type", "expected_count"), - [ - (1, 28), - (2, 33), - ], -) -async def test_async_setup_entry( - hass: HomeAssistant, - mock_config_entry, - mock_coordinator, - device_type, - expected_count, -) -> None: - """Test async_setup_entry creates correct sensors.""" - mock_coordinator.data.device_type = device_type - mock_config_entry.runtime_data = mock_coordinator - entities_added = [] - - def mock_add_entities(entities): - entities_added.extend(entities) - - await async_setup_entry(hass, mock_config_entry, mock_add_entities) - - # Verify all 24 sensors were added - assert len(entities_added) == expected_count - - # Verify sensor types - sensor_types = [type(entity).__name__ for entity in entities_added] - assert "FilterRemainingSensor" in sensor_types - assert "CO2RemainingSensor" in sensor_types - assert "TapStateSensor" in sensor_types - assert "CleanModeStateSensor" in sensor_types - assert "ErrorBitsSensor" in sensor_types - assert "FilterLifetimeSensor" in sensor_types - assert "PostFlushQuantitySensor" in sensor_types - assert "FirmwareMainSensor" in sensor_types - assert "FirmwareCommSensor" in sensor_types - assert "FirmwareElecSensor" in sensor_types - assert "DeviceNameSensor" in sensor_types - assert "ResetCountSensor" in sensor_types - assert "DeviceTypeSensor" in sensor_types - assert "SerialNumberSensor" in sensor_types - assert "ServiceCodeSensor" in sensor_types - assert "WiFiSSIDSensor" in sensor_types - assert "WiFiSignalSensor" in sensor_types - assert "IPAddressSensor" in sensor_types - assert "BLEMacSensor" in sensor_types - assert "WiFiMacSensor" in sensor_types - assert "GatewaySensor" in sensor_types - assert "GatewayMacSensor" in sensor_types - assert "SubnetSensor" in sensor_types - if device_type == 2: - assert "BoilerTemp1Sensor" in sensor_types - assert "BoilerTemp2Sensor" in sensor_types - assert "CoolingTempSensor" in sensor_types - assert "MainControllerStatusSensor" in sensor_types - assert "ConnControllerStatusSensor" in sensor_types - - -async def test_filter_remaining_sensor(mock_coordinator) -> None: - """Test FilterRemainingSensor.""" - sensor = FilterRemainingSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 85 - assert sensor.unique_id == "filter_remaining" - - -async def test_filter_remaining_sensor_unavailable(mock_coordinator) -> None: - """Test FilterRemainingSensor when status is None.""" - mock_coordinator.data.status = None - sensor = FilterRemainingSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_co2_remaining_sensor(mock_coordinator) -> None: - """Test CO2RemainingSensor.""" - sensor = CO2RemainingSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 90 - assert sensor.unique_id == "co2_remaining" - - -async def test_co2_remaining_sensor_unavailable(mock_coordinator) -> None: - """Test CO2RemainingSensor when status is None.""" - mock_coordinator.data.status = None - sensor = CO2RemainingSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_tap_state_sensor(mock_coordinator) -> None: - """Test TapStateSensor.""" - sensor = TapStateSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 1 - assert sensor.unique_id == "tap_state" - - -async def test_tap_state_sensor_unavailable(mock_coordinator) -> None: - """Test TapStateSensor when status is None.""" - mock_coordinator.data.status = None - sensor = TapStateSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_clean_mode_state_sensor(mock_coordinator) -> None: - """Test CleanModeStateSensor.""" - sensor = CleanModeStateSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 0 - assert sensor.unique_id == "clean_mode_state" - - -async def test_clean_mode_state_sensor_unavailable(mock_coordinator) -> None: - """Test CleanModeStateSensor when status is None.""" - mock_coordinator.data.status = None - sensor = CleanModeStateSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_error_bits_sensor(mock_coordinator) -> None: - """Test ErrorBitsSensor.""" - sensor = ErrorBitsSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 0 - assert sensor.unique_id == "error_bits" - - -async def test_error_bits_sensor_unavailable(mock_coordinator) -> None: - """Test ErrorBitsSensor when status is None.""" - mock_coordinator.data.status = None - sensor = ErrorBitsSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_filter_lifetime_sensor(mock_coordinator) -> None: - """Test FilterLifetimeSensor.""" - sensor = FilterLifetimeSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 365 - assert sensor.unique_id == "filter_lifetime" - - -async def test_filter_lifetime_sensor_unavailable(mock_coordinator) -> None: - """Test FilterLifetimeSensor when settings is None.""" - mock_coordinator.data.settings = None - sensor = FilterLifetimeSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_post_flush_quantity_sensor(mock_coordinator) -> None: - """Test PostFlushQuantitySensor.""" - sensor = PostFlushQuantitySensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 100 - assert sensor.unique_id == "post_flush_quantity" - - -async def test_post_flush_quantity_sensor_unavailable(mock_coordinator) -> None: - """Test PostFlushQuantitySensor when settings is None.""" - mock_coordinator.data.settings = None - sensor = PostFlushQuantitySensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_firmware_main_sensor(mock_coordinator) -> None: - """Test FirmwareMainSensor.""" - sensor = FirmwareMainSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "1.2.0" - assert sensor.unique_id == "firmware_main" - - -async def test_firmware_main_sensor_unavailable(mock_coordinator) -> None: - """Test FirmwareMainSensor when system_info is None.""" - mock_coordinator.data.system_info = None - sensor = FirmwareMainSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_firmware_comm_sensor(mock_coordinator) -> None: - """Test FirmwareCommSensor.""" - sensor = FirmwareCommSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "1.0.0" - assert sensor.unique_id == "firmware_comm" - - -async def test_firmware_comm_sensor_unavailable(mock_coordinator) -> None: - """Test FirmwareCommSensor when system_info is None.""" - mock_coordinator.data.system_info = None - sensor = FirmwareCommSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_firmware_elec_sensor(mock_coordinator) -> None: - """Test FirmwareElecSensor.""" - sensor = FirmwareElecSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "1.1.0" - assert sensor.unique_id == "firmware_elec" - - -async def test_firmware_elec_sensor_unavailable(mock_coordinator) -> None: - """Test FirmwareElecSensor when system_info is None.""" - mock_coordinator.data.system_info = None - sensor = FirmwareElecSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_device_name_sensor(mock_coordinator) -> None: - """Test DeviceNameSensor.""" - sensor = DeviceNameSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "Test Device" - assert sensor.unique_id == "device_name" - - -async def test_device_name_sensor_unavailable(mock_coordinator) -> None: - """Test DeviceNameSensor when system_info is None.""" - mock_coordinator.data.system_info = None - sensor = DeviceNameSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_reset_count_sensor(mock_coordinator) -> None: - """Test ResetCountSensor.""" - sensor = ResetCountSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == 10 - assert sensor.unique_id == "reset_count" - - -async def test_reset_count_sensor_unavailable(mock_coordinator) -> None: - """Test ResetCountSensor when system_info is None.""" - mock_coordinator.data.system_info = None - sensor = ResetCountSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_device_type_sensor(mock_coordinator) -> None: - """Test DeviceTypeSensor.""" - sensor = DeviceTypeSensor(mock_coordinator) - - assert sensor.native_value == 1 - assert sensor.unique_id == "device_type" - - -async def test_device_type_sensor_none(mock_coordinator) -> None: - """Test DeviceTypeSensor when device_type is None.""" - mock_coordinator.data.device_type = None - sensor = DeviceTypeSensor(mock_coordinator) - - assert sensor.native_value is None - - -async def test_serial_number_sensor(mock_coordinator) -> None: - """Test SerialNumberSensor.""" - sensor = SerialNumberSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "123456" - assert sensor.unique_id == "serial_number" - - -async def test_serial_number_sensor_unavailable(mock_coordinator) -> None: - """Test SerialNumberSensor when identity is None.""" - mock_coordinator.data.identity = None - sensor = SerialNumberSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_service_code_sensor(mock_coordinator) -> None: - """Test ServiceCodeSensor.""" - sensor = ServiceCodeSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "ABCDEF" - assert sensor.unique_id == "service_code" - - -async def test_service_code_sensor_unavailable(mock_coordinator) -> None: - """Test ServiceCodeSensor when identity is None.""" - mock_coordinator.data.identity = None - sensor = ServiceCodeSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_wifi_ssid_sensor(mock_coordinator) -> None: - """Test WiFiSSIDSensor.""" - sensor = WiFiSSIDSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "TestSSID" - assert sensor.unique_id == "wifi_ssid" - - -async def test_wifi_ssid_sensor_unavailable(mock_coordinator) -> None: - """Test WiFiSSIDSensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = WiFiSSIDSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_wifi_signal_sensor(mock_coordinator) -> None: - """Test WiFiSignalSensor.""" - sensor = WiFiSignalSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == -50 - assert sensor.unique_id == "wifi_signal" - - -async def test_wifi_signal_sensor_unavailable(mock_coordinator) -> None: - """Test WiFiSignalSensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = WiFiSignalSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_ip_address_sensor(mock_coordinator) -> None: - """Test IPAddressSensor.""" - sensor = IPAddressSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "192.168.1.100" - assert sensor.unique_id == "ip_address" - - -async def test_ip_address_sensor_unavailable(mock_coordinator) -> None: - """Test IPAddressSensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = IPAddressSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_ble_mac_sensor(mock_coordinator) -> None: - """Test BLEMacSensor.""" - sensor = BLEMacSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "AA:BB:CC:DD:EE:FF" - assert sensor.unique_id == "ble_mac" - - -async def test_ble_mac_sensor_unavailable(mock_coordinator) -> None: - """Test BLEMacSensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = BLEMacSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_wifi_mac_sensor(mock_coordinator) -> None: - """Test WiFiMacSensor.""" - sensor = WiFiMacSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "11:22:33:44:55:66" - assert sensor.unique_id == "wifi_mac" - - -async def test_wifi_mac_sensor_unavailable(mock_coordinator) -> None: - """Test WiFiMacSensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = WiFiMacSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_gateway_sensor(mock_coordinator) -> None: - """Test GatewaySensor.""" - sensor = GatewaySensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "192.168.1.1" - assert sensor.unique_id == "gateway" - - -async def test_gateway_sensor_unavailable(mock_coordinator) -> None: - """Test GatewaySensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = GatewaySensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_gateway_mac_sensor(mock_coordinator) -> None: - """Test GatewayMacSensor.""" - sensor = GatewayMacSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "AA:BB:CC:DD:EE:00" - assert sensor.unique_id == "gateway_mac" - - -async def test_gateway_mac_sensor_unavailable(mock_coordinator) -> None: - """Test GatewayMacSensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = GatewayMacSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_subnet_sensor(mock_coordinator) -> None: - """Test SubnetSensor.""" - sensor = SubnetSensor(mock_coordinator) - - assert sensor.available is True - assert sensor.native_value == "255.255.255.0" - assert sensor.unique_id == "subnet" - - -async def test_subnet_sensor_unavailable(mock_coordinator) -> None: - """Test SubnetSensor when wifi_info is None.""" - mock_coordinator.data.wifi_info = None - sensor = SubnetSensor(mock_coordinator) - - assert sensor.available is False - assert sensor.native_value is None - - -async def test_sensor_when_data_is_unavailable(mock_coordinator) -> None: - """Test sensor when data is unavailable.""" - mock_coordinator.data.available = False - sensor = FilterRemainingSensor(mock_coordinator) - - assert sensor.available is False +"""Tests for the Blanco Unit sensor entities.""" + +from unittest.mock import MagicMock, patch + +import pytest +from pytest_homeassistant_custom_component.common import ( + MockConfigEntry, + snapshot_platform, +) +from syrupy.assertion import SnapshotAssertion + +from custom_components.blanco_unit.const import CONF_MAC, CONF_NAME, CONF_PIN, DOMAIN +from custom_components.blanco_unit.data import ( + BlancoUnitData, + BlancoUnitIdentity, + BlancoUnitSettings, + BlancoUnitStatus, + BlancoUnitSystemInfo, + BlancoUnitWifiInfo, +) +from custom_components.blanco_unit.sensor import ( + BLEMacSensor, + CleanModeStateSensor, + CO2RemainingSensor, + DeviceNameSensor, + DeviceTypeSensor, + ErrorBitsSensor, + FilterLifetimeSensor, + FilterRemainingSensor, + FirmwareCommSensor, + FirmwareElecSensor, + FirmwareMainSensor, + GatewayMacSensor, + GatewaySensor, + IPAddressSensor, + PostFlushQuantitySensor, + ResetCountSensor, + SerialNumberSensor, + ServiceCodeSensor, + SubnetSensor, + TapStateSensor, + WiFiMacSensor, + WiFiSignalSensor, + WiFiSSIDSensor, + async_setup_entry, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import setup_integration # noqa: TID251 + + +@pytest.fixture +def mock_coordinator(): + """Create a mock coordinator.""" + coordinator = MagicMock() + coordinator.data = BlancoUnitData( + connected=True, + available=True, + device_id="test_device_id", + device_type=1, + status=BlancoUnitStatus( + tap_state=1, + filter_rest=85, + co2_rest=90, + wtr_disp_active=True, + firm_upd_avlb=False, + set_point_cooling=7, + clean_mode_state=0, + err_bits=0, + ), + settings=BlancoUnitSettings( + calib_still_wtr=5, + calib_soda_wtr=5, + filter_life_tm=365, + post_flush_quantity=100, + set_point_cooling=7, + wtr_hardness=5, + ), + system_info=BlancoUnitSystemInfo( + sw_ver_comm_con="1.0.0", + sw_ver_elec_con="1.1.0", + sw_ver_main_con="1.2.0", + dev_name="Test Device", + reset_cnt=10, + ), + identity=BlancoUnitIdentity( + serial_no="123456", + service_code="ABCDEF", + ), + wifi_info=BlancoUnitWifiInfo( + cloud_connect=True, + ssid="TestSSID", + signal=-50, + ip="192.168.1.100", + ble_mac="AA:BB:CC:DD:EE:FF", + wifi_mac="11:22:33:44:55:66", + gateway="192.168.1.1", + gateway_mac="AA:BB:CC:DD:EE:00", + subnet="255.255.255.0", + ), + ) + return coordinator + + +@pytest.fixture +def mock_config_entry(): + """Create a mock config entry.""" + entry = MagicMock() + entry.entry_id = "test_entry_id" + return entry + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_blanco_unit_data, + mock_bluetooth_device, +) -> None: + """Test all entities.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_NAME: "Test Blanco Unit", + CONF_PIN: 12345, + }, + unique_id="AA:BB:CC:DD:EE:FF", + ) + + with ( + patch("custom_components.blanco_unit.PLATFORMS", [Platform.SENSOR]), + patch( + "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", + return_value=mock_bluetooth_device, + ), + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.parametrize( + ("device_type", "expected_count"), + [ + (1, 28), + (2, 33), + ], +) +async def test_async_setup_entry( + hass: HomeAssistant, + mock_config_entry, + mock_coordinator, + device_type, + expected_count, +) -> None: + """Test async_setup_entry creates correct sensors.""" + mock_coordinator.data.device_type = device_type + mock_config_entry.runtime_data = mock_coordinator + entities_added = [] + + def mock_add_entities(entities): + entities_added.extend(entities) + + await async_setup_entry(hass, mock_config_entry, mock_add_entities) + + # Verify all 24 sensors were added + assert len(entities_added) == expected_count + + # Verify sensor types + sensor_types = [type(entity).__name__ for entity in entities_added] + assert "FilterRemainingSensor" in sensor_types + assert "CO2RemainingSensor" in sensor_types + assert "TapStateSensor" in sensor_types + assert "CleanModeStateSensor" in sensor_types + assert "ErrorBitsSensor" in sensor_types + assert "FilterLifetimeSensor" in sensor_types + assert "PostFlushQuantitySensor" in sensor_types + assert "FirmwareMainSensor" in sensor_types + assert "FirmwareCommSensor" in sensor_types + assert "FirmwareElecSensor" in sensor_types + assert "DeviceNameSensor" in sensor_types + assert "ResetCountSensor" in sensor_types + assert "DeviceTypeSensor" in sensor_types + assert "SerialNumberSensor" in sensor_types + assert "ServiceCodeSensor" in sensor_types + assert "WiFiSSIDSensor" in sensor_types + assert "WiFiSignalSensor" in sensor_types + assert "IPAddressSensor" in sensor_types + assert "BLEMacSensor" in sensor_types + assert "WiFiMacSensor" in sensor_types + assert "GatewaySensor" in sensor_types + assert "GatewayMacSensor" in sensor_types + assert "SubnetSensor" in sensor_types + if device_type == 2: + assert "BoilerTemp1Sensor" in sensor_types + assert "BoilerTemp2Sensor" in sensor_types + assert "CoolingTempSensor" in sensor_types + assert "MainControllerStatusSensor" in sensor_types + assert "ConnControllerStatusSensor" in sensor_types + + +async def test_filter_remaining_sensor(mock_coordinator) -> None: + """Test FilterRemainingSensor.""" + sensor = FilterRemainingSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 85 + assert sensor.unique_id == "filter_remaining" + + +async def test_filter_remaining_sensor_unavailable(mock_coordinator) -> None: + """Test FilterRemainingSensor when status is None.""" + mock_coordinator.data.status = None + sensor = FilterRemainingSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_co2_remaining_sensor(mock_coordinator) -> None: + """Test CO2RemainingSensor.""" + sensor = CO2RemainingSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 90 + assert sensor.unique_id == "co2_remaining" + + +async def test_co2_remaining_sensor_unavailable(mock_coordinator) -> None: + """Test CO2RemainingSensor when status is None.""" + mock_coordinator.data.status = None + sensor = CO2RemainingSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_tap_state_sensor(mock_coordinator) -> None: + """Test TapStateSensor.""" + sensor = TapStateSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 1 + assert sensor.unique_id == "tap_state" + + +async def test_tap_state_sensor_unavailable(mock_coordinator) -> None: + """Test TapStateSensor when status is None.""" + mock_coordinator.data.status = None + sensor = TapStateSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_clean_mode_state_sensor(mock_coordinator) -> None: + """Test CleanModeStateSensor.""" + sensor = CleanModeStateSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 0 + assert sensor.unique_id == "clean_mode_state" + + +async def test_clean_mode_state_sensor_unavailable(mock_coordinator) -> None: + """Test CleanModeStateSensor when status is None.""" + mock_coordinator.data.status = None + sensor = CleanModeStateSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_error_bits_sensor(mock_coordinator) -> None: + """Test ErrorBitsSensor.""" + sensor = ErrorBitsSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 0 + assert sensor.unique_id == "error_bits" + + +async def test_error_bits_sensor_unavailable(mock_coordinator) -> None: + """Test ErrorBitsSensor when status is None.""" + mock_coordinator.data.status = None + sensor = ErrorBitsSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_filter_lifetime_sensor(mock_coordinator) -> None: + """Test FilterLifetimeSensor.""" + sensor = FilterLifetimeSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 365 + assert sensor.unique_id == "filter_lifetime" + + +async def test_filter_lifetime_sensor_unavailable(mock_coordinator) -> None: + """Test FilterLifetimeSensor when settings is None.""" + mock_coordinator.data.settings = None + sensor = FilterLifetimeSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_post_flush_quantity_sensor(mock_coordinator) -> None: + """Test PostFlushQuantitySensor.""" + sensor = PostFlushQuantitySensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 100 + assert sensor.unique_id == "post_flush_quantity" + + +async def test_post_flush_quantity_sensor_unavailable(mock_coordinator) -> None: + """Test PostFlushQuantitySensor when settings is None.""" + mock_coordinator.data.settings = None + sensor = PostFlushQuantitySensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_firmware_main_sensor(mock_coordinator) -> None: + """Test FirmwareMainSensor.""" + sensor = FirmwareMainSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "1.2.0" + assert sensor.unique_id == "firmware_main" + + +async def test_firmware_main_sensor_unavailable(mock_coordinator) -> None: + """Test FirmwareMainSensor when system_info is None.""" + mock_coordinator.data.system_info = None + sensor = FirmwareMainSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_firmware_comm_sensor(mock_coordinator) -> None: + """Test FirmwareCommSensor.""" + sensor = FirmwareCommSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "1.0.0" + assert sensor.unique_id == "firmware_comm" + + +async def test_firmware_comm_sensor_unavailable(mock_coordinator) -> None: + """Test FirmwareCommSensor when system_info is None.""" + mock_coordinator.data.system_info = None + sensor = FirmwareCommSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_firmware_elec_sensor(mock_coordinator) -> None: + """Test FirmwareElecSensor.""" + sensor = FirmwareElecSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "1.1.0" + assert sensor.unique_id == "firmware_elec" + + +async def test_firmware_elec_sensor_unavailable(mock_coordinator) -> None: + """Test FirmwareElecSensor when system_info is None.""" + mock_coordinator.data.system_info = None + sensor = FirmwareElecSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_device_name_sensor(mock_coordinator) -> None: + """Test DeviceNameSensor.""" + sensor = DeviceNameSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "Test Device" + assert sensor.unique_id == "device_name" + + +async def test_device_name_sensor_unavailable(mock_coordinator) -> None: + """Test DeviceNameSensor when system_info is None.""" + mock_coordinator.data.system_info = None + sensor = DeviceNameSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_reset_count_sensor(mock_coordinator) -> None: + """Test ResetCountSensor.""" + sensor = ResetCountSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == 10 + assert sensor.unique_id == "reset_count" + + +async def test_reset_count_sensor_unavailable(mock_coordinator) -> None: + """Test ResetCountSensor when system_info is None.""" + mock_coordinator.data.system_info = None + sensor = ResetCountSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_device_type_sensor(mock_coordinator) -> None: + """Test DeviceTypeSensor.""" + sensor = DeviceTypeSensor(mock_coordinator) + + assert sensor.native_value == 1 + assert sensor.unique_id == "device_type" + + +async def test_device_type_sensor_none(mock_coordinator) -> None: + """Test DeviceTypeSensor when device_type is None.""" + mock_coordinator.data.device_type = None + sensor = DeviceTypeSensor(mock_coordinator) + + assert sensor.native_value is None + + +async def test_serial_number_sensor(mock_coordinator) -> None: + """Test SerialNumberSensor.""" + sensor = SerialNumberSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "123456" + assert sensor.unique_id == "serial_number" + + +async def test_serial_number_sensor_unavailable(mock_coordinator) -> None: + """Test SerialNumberSensor when identity is None.""" + mock_coordinator.data.identity = None + sensor = SerialNumberSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_service_code_sensor(mock_coordinator) -> None: + """Test ServiceCodeSensor.""" + sensor = ServiceCodeSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "ABCDEF" + assert sensor.unique_id == "service_code" + + +async def test_service_code_sensor_unavailable(mock_coordinator) -> None: + """Test ServiceCodeSensor when identity is None.""" + mock_coordinator.data.identity = None + sensor = ServiceCodeSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_wifi_ssid_sensor(mock_coordinator) -> None: + """Test WiFiSSIDSensor.""" + sensor = WiFiSSIDSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "TestSSID" + assert sensor.unique_id == "wifi_ssid" + + +async def test_wifi_ssid_sensor_unavailable(mock_coordinator) -> None: + """Test WiFiSSIDSensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = WiFiSSIDSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_wifi_signal_sensor(mock_coordinator) -> None: + """Test WiFiSignalSensor.""" + sensor = WiFiSignalSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == -50 + assert sensor.unique_id == "wifi_signal" + + +async def test_wifi_signal_sensor_unavailable(mock_coordinator) -> None: + """Test WiFiSignalSensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = WiFiSignalSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_ip_address_sensor(mock_coordinator) -> None: + """Test IPAddressSensor.""" + sensor = IPAddressSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "192.168.1.100" + assert sensor.unique_id == "ip_address" + + +async def test_ip_address_sensor_unavailable(mock_coordinator) -> None: + """Test IPAddressSensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = IPAddressSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_ble_mac_sensor(mock_coordinator) -> None: + """Test BLEMacSensor.""" + sensor = BLEMacSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "AA:BB:CC:DD:EE:FF" + assert sensor.unique_id == "ble_mac" + + +async def test_ble_mac_sensor_unavailable(mock_coordinator) -> None: + """Test BLEMacSensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = BLEMacSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_wifi_mac_sensor(mock_coordinator) -> None: + """Test WiFiMacSensor.""" + sensor = WiFiMacSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "11:22:33:44:55:66" + assert sensor.unique_id == "wifi_mac" + + +async def test_wifi_mac_sensor_unavailable(mock_coordinator) -> None: + """Test WiFiMacSensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = WiFiMacSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_gateway_sensor(mock_coordinator) -> None: + """Test GatewaySensor.""" + sensor = GatewaySensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "192.168.1.1" + assert sensor.unique_id == "gateway" + + +async def test_gateway_sensor_unavailable(mock_coordinator) -> None: + """Test GatewaySensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = GatewaySensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_gateway_mac_sensor(mock_coordinator) -> None: + """Test GatewayMacSensor.""" + sensor = GatewayMacSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "AA:BB:CC:DD:EE:00" + assert sensor.unique_id == "gateway_mac" + + +async def test_gateway_mac_sensor_unavailable(mock_coordinator) -> None: + """Test GatewayMacSensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = GatewayMacSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_subnet_sensor(mock_coordinator) -> None: + """Test SubnetSensor.""" + sensor = SubnetSensor(mock_coordinator) + + assert sensor.available is True + assert sensor.native_value == "255.255.255.0" + assert sensor.unique_id == "subnet" + + +async def test_subnet_sensor_unavailable(mock_coordinator) -> None: + """Test SubnetSensor when wifi_info is None.""" + mock_coordinator.data.wifi_info = None + sensor = SubnetSensor(mock_coordinator) + + assert sensor.available is False + assert sensor.native_value is None + + +async def test_sensor_when_data_is_unavailable(mock_coordinator) -> None: + """Test sensor when data is unavailable.""" + mock_coordinator.data.available = False + sensor = FilterRemainingSensor(mock_coordinator) + + assert sensor.available is False From 13c63a38669e5493da66096bcdfa8465ccdd1572 Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:48:45 +0100 Subject: [PATCH 8/9] update readme --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b11ebf7..41e3c22 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ The integration polls the device every **60 seconds** to update sensor values an Once configured, the integration creates the following entities: -### Sensors (27 total) +### Sensors (32 total) #### Status Sensors @@ -106,6 +106,14 @@ Once configured, the integration creates the following entities: - **Filter Lifetime** - Configured filter lifetime in days - **Post-Flush Quantity** - Post-flush water quantity in mL +#### CHOICE.All Status Sensors + +- **Boiler Temperature 1** - Boiler temperature sensor 1 (°C) +- **Boiler Temperature 2** - Boiler temperature sensor 2 (°C) +- **Cooling Temperature** - Compressor/condenser temperature (°C), idles at ~32-34°C, spikes to ~52-55°C when compressor is running +- **Main Controller Status** - Raw main controller status value +- **Connection Controller Status** - Raw connection controller status value + #### Firmware Sensors - **Main Controller Firmware** - Main controller firmware version @@ -130,13 +138,18 @@ Once configured, the integration creates the following entities: - **Gateway MAC Address** - Gateway MAC address - **Subnet Mask** - Network subnet mask -### Binary Sensors (4 total) +### Binary Sensors (6 total) - **BLE Connection** - Bluetooth connection status - **Water Dispensing** - Whether water is currently being dispensed - **Firmware Update Available** - Whether a firmware update is available - **Cloud Connection** - Cloud service connection status +#### CHOICE.All Binary Sensors + +- **Heater Active** - Whether the boiler heater element is currently active (decoded from main controller status bit 13) +- **Compressor Active** - Whether the cooling compressor is currently running (decoded from main controller status bit 14) + ### Buttons (2 total) - **Disconnect** - Manually disconnect from the device @@ -147,11 +160,15 @@ Once configured, the integration creates the following entities: - **Still Water Calibration** - Calibration value for still water (1-10 mL) - **Soda Water Calibration** - Calibration value for carbonated water (1-10 mL) -### Select Entities (2 total) +### Select Entities (3 total) - **Cooling Temperature** - Target water temperature (4-10°C) - Options: 4°C (coldest), 5°C, 6°C, 7°C (recommended), 8°C, 9°C, 10°C (warmest) - **Water Hardness Level** - Water hardness setting (1-9) + +#### CHOICE.All Select Entities + +- **Heating Temperature** - Target hot water temperature (60-100°C) - Level 1: <8°dH - Level 2: 8-10°dH - Level 3: 11-13°dH From 88dbae471d83561b6d7664cfd9b0be789d766cc0 Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:01:15 +0100 Subject: [PATCH 9/9] update bluetooth protocol information --- BLUETOOTH_PROTOCOL.md | 88 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/BLUETOOTH_PROTOCOL.md b/BLUETOOTH_PROTOCOL.md index 154622f..bb644ef 100644 --- a/BLUETOOTH_PROTOCOL.md +++ b/BLUETOOTH_PROTOCOL.md @@ -274,8 +274,8 @@ When using event type 7 with control code 3, you must specify a sub-event type i | -------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | 2 | System information | Firmware versions (`sw_ver_comm_con`, `sw_ver_elec_con`, `sw_ver_main_con`), device name, reset count | | 4 | Error information | Array of errors with `err_code` and `err_msg` (empty array if no errors) | -| 5 | Device settings | Calibration values, filter lifetime, post-flush quantity, temperature setpoint, water hardness | -| 6 | Device status | Tap state, filter/CO2 remaining percentage, water dispensing active, firmware update available, cleaning mode, error bits | +| 5 | Device settings | Calibration values, filter lifetime, post-flush quantity, temperature setpoint, water hardness. CHOICE.All adds: heating setpoint, hot water calibration, carbonation ratios | +| 6 | Device status | Tap state, filter/CO2 remaining percentage, water dispensing active, firmware update available, cleaning mode, error bits. CHOICE.All adds: boiler temperatures, compressor temperature, controller status values | ### Settings Parameters (evt_type=7, ctrl=5) @@ -283,14 +283,62 @@ When setting device configuration, use event type 7 with control code 5 and one | Setting | Parameter Format | Valid Values | Description | | ----------------------- | -------------------------------------------------------------------------- | ------------ | ------------------------------------------------------- | -| Cooling temperature | `{"set_point_cooling": {"val": }, "set_point_heating": {"val": 65}}` | 4-10°C | Set target cooling temperature (heating is always 65°C) | +| Cooling temperature | `{"set_point_cooling": {"val": }}` | 4-10°C | Set target cooling temperature | | Water hardness | `{"wtr_hardness": {"val": }}` | 1-9 | Set water hardness level | | Still water calibration | `{"calib_still_wtr": {"val": }}` | 1-10 | Calibrate still water flow | | Soda water calibration | `{"calib_soda_wtr": {"val": }}` | 1-10 | Calibrate carbonated water flow | ## Event Types (Blanco Choice.all) -Yet to be defined. +The Blanco CHOICE.All (device type `dev_type: 2`) uses the same event types, control codes, and message format as the Drink.soda (`dev_type: 1`). The key differences are additional parameters in responses and additional write operations. + +### Device Type Identification + +The device type is returned in the pairing response (`evt_type: 10`) in the `meta.dev_type` field: + +- `dev_type: 1` = Blanco Drink.soda +- `dev_type: 2` = Blanco CHOICE.All + +### Additional Status Fields (evt_type=6) + +The CHOICE.All status response includes these additional fields alongside the standard Drink.soda fields: + +| Parameter | Type | Description | +| -------------------------- | ------- | ----------------------------------------------------------------------------------------------- | +| `temp_boil_1` | integer | Boiler temperature sensor 1 (°C) | +| `temp_boil_2` | integer | Boiler temperature sensor 2 (°C) | +| `temp_comp` | integer | Compressor/condenser temperature (°C) - idles at ~32-34°C, spikes to ~52-55°C when running | +| `main_controller_status` | integer | Main controller status bitmask (see bit definitions below) | +| `conn_controller_status` | integer | Connection controller status value | + +#### Main Controller Status Bitmask + +The `main_controller_status` field is a bitmask. Known bits: + +| Bit | Hex Value | Description | +| ---- | --------- | ---------------------------------------------------------------------- | +| 8+16 | 0x10100 | Base state bits, always set when device is running (value: 65792) | +| 13 | 0x2000 | Boiler heater element active (heating water to setpoint) | +| 14 | 0x4000 | Cooling compressor active (compressor running to cool water) | + +**Note:** Heater and compressor never run simultaneously (load management). + +### Additional Settings Fields (evt_type=5) + +The CHOICE.All settings response includes these additional fields: + +| Parameter | Type | Description | +| ---------------------- | ----- | -------------------------------------- | +| `set_point_heating` | int | Heating setpoint temperature (60-100°C)| +| `calib_hot_wtr` | int | Hot water calibration value (mL) | +| `gbl_medium_wtr_ratio` | float | Medium carbonation water ratio | +| `gbl_classic_wtr_ratio`| float | Classic carbonation water ratio | + +### Additional Settings Parameters (evt_type=7, ctrl=5) + +| Setting | Parameter Format | Valid Values | Description | +| -------------------- | ------------------------------------------- | ------------ | --------------------------------- | +| Heating temperature | `{"set_point_heating": {"val": }}` | 60-100°C | Set target heating temperature | ## Request/Response Examples @@ -1012,6 +1060,10 @@ Retrieve device configuration settings. - `post_flush_quantity`: Post-flush quantity in mL - `set_point_cooling`: Target cooling temperature (4-10°C) - `wtr_hardness`: Water hardness level (1-9) +- `set_point_heating`: Heating setpoint temperature in °C (CHOICE.All only, 0 for drink.soda) +- `calib_hot_wtr`: Hot water calibration in mL (CHOICE.All only) +- `gbl_medium_wtr_ratio`: Medium carbonation water ratio (CHOICE.All only) +- `gbl_classic_wtr_ratio`: Classic carbonation water ratio (CHOICE.All only) #### `get_status() -> BlancoUnitStatus` @@ -1029,6 +1081,11 @@ Retrieve real-time device status. - `set_point_cooling`: Current temperature setting - `clean_mode_state`: Cleaning mode state - `err_bits`: Error code bits +- `temp_boil_1`: Boiler temperature sensor 1 in °C (CHOICE.All only) +- `temp_boil_2`: Boiler temperature sensor 2 in °C (CHOICE.All only) +- `temp_comp`: Compressor/condenser temperature in °C (CHOICE.All only) +- `main_controller_status`: Main controller status bitmask (CHOICE.All only) +- `conn_controller_status`: Connection controller status (CHOICE.All only) #### `get_device_identity() -> BlancoUnitIdentity` @@ -1073,6 +1130,18 @@ Set cooling temperature. **Returns:** True if successful +#### `set_heating_temperature(heating_celsius: int) -> bool` + +Set heating/boiling temperature (CHOICE.All only). + +**Protocol:** Event type 7, control 5, pars `{"set_point_heating": {"val": }}` + +**Parameters:** + +- `heating_celsius`: Temperature in Celsius (60-100) + +**Returns:** True if successful + #### `set_water_hardness(level: int) -> bool` Set water hardness level. @@ -1161,6 +1230,11 @@ class BlancoUnitSettings: post_flush_quantity: int # Post-flush quantity (mL) set_point_cooling: int # Temperature setting (4-10°C) wtr_hardness: int # Water hardness level (1-9) + # CHOICE.All specific fields (default to 0 for drink.soda) + set_point_heating: int = 0 # Heating setpoint (60-100°C) + calib_hot_wtr: int = 0 # Hot water calibration (mL) + gbl_medium_wtr_ratio: float = 0.0 # Medium carbonation water ratio + gbl_classic_wtr_ratio: float = 0.0 # Classic carbonation water ratio ``` ### BlancoUnitStatus @@ -1176,6 +1250,12 @@ class BlancoUnitStatus: set_point_cooling: int # Current temperature clean_mode_state: int # Cleaning mode state err_bits: int # Error bits + # CHOICE.All specific fields (default to 0 for drink.soda) + temp_boil_1: int = 0 # Boiler temperature sensor 1 (°C) + temp_boil_2: int = 0 # Boiler temperature sensor 2 (°C) + temp_comp: int = 0 # Compressor/condenser temperature (°C) + main_controller_status: int = 0 # Main controller status bitmask + conn_controller_status: int = 0 # Connection controller status ``` ### BlancoUnitIdentity