diff --git a/.coverage b/.coverage index 9c5e334..977de2d 100644 Binary files a/.coverage and b/.coverage differ 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 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 diff --git a/custom_components/blanco_unit/binary_sensor.py b/custom_components/blanco_unit/binary_sensor.py index 429f28f..66b7743 100644 --- a/custom_components/blanco_unit/binary_sensor.py +++ b/custom_components/blanco_unit/binary_sensor.py @@ -12,6 +12,11 @@ from .base import BlancoUnitBaseEntity 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, @@ -20,14 +25,21 @@ async def async_setup_entry( ) -> 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 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): @@ -120,3 +132,58 @@ 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 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 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/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..77da7f3 100644 --- a/custom_components/blanco_unit/select.py +++ b/custom_components/blanco_unit/select.py @@ -19,12 +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), - 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): @@ -57,6 +58,42 @@ 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 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 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..76a398b 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 @@ -27,8 +28,7 @@ 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), @@ -38,6 +38,11 @@ async def async_setup_entry( # 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), @@ -59,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) # ------------------------------- @@ -174,6 +189,126 @@ 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 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:heat-wave" + + @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 +360,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 # ------------------------------- @@ -359,7 +587,7 @@ class DeviceIdSensor(BlancoUnitBaseEntity, SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC @property - def native_value(self) -> int | None: + def native_value(self) -> str | None: """Return the device id.""" return self.coordinator.data.device_id diff --git a/custom_components/blanco_unit/translations/en.json b/custom_components/blanco_unit/translations/en.json index b9ecac6..bdfc859 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}." } @@ -92,6 +95,12 @@ }, "cloud_connection": { "name": "Cloud Connection" + }, + "heater_active": { + "name": "Heater Active" + }, + "compressor_active": { + "name": "Compressor Active" } }, "button": { @@ -123,6 +132,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 +178,39 @@ "error_bits": { "name": "Error Code" }, + "boiler_temp_1": { + "name": "Boiler Temperature 1" + }, + "boiler_temp_2": { + "name": "Boiler Temperature 2" + }, + "cooling_temp": { + "name": "Compressor 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" }, @@ -178,6 +229,9 @@ "device_type": { "name": "Device Type" }, + "device_id": { + "name": "Device ID" + }, "serial_number": { "name": "Serial Number" }, 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 6d08e4c..ab88d38 100644 --- a/tests/snapshots/test_sensor.ambr +++ b/tests/snapshots/test_sensor.ambr @@ -1,1202 +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_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_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_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_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_none-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_none', - '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': None, - '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_none-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Blanco Unit None', - 'icon': 'mdi:information', - }), - 'context': , - 'entity_id': 'sensor.test_blanco_unit_none', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'test_device_id', - }) -# --- -# 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_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 diff --git a/tests/test_select.py b/tests/test_select.py index a6b9ae1..0ccdd9d 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -1,257 +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 - ) - - -async def test_async_setup_entry( - hass: HomeAssistant, mock_config_entry, mock_coordinator -) -> None: - """Test async_setup_entry creates all select entities.""" - 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 both select entities were added - assert len(entities_added) == 2 - - # Verify entity types - entity_types = [type(entity).__name__ for entity in entities_added] - assert "TemperatureSelect" in entity_types - assert "WaterHardnessSelect" 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 a864339..6de3cc9 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,606 +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 - ) - - -async def test_async_setup_entry( - hass: HomeAssistant, mock_config_entry, mock_coordinator -) -> None: - """Test async_setup_entry creates all sensors.""" - 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) == 24 - - # 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 - - -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