Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .coverage
Binary file not shown.
88 changes: 84 additions & 4 deletions BLUETOOTH_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,23 +274,71 @@ 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)

When setting device configuration, use event type 7 with control code 5 and one of these parameters:

| Setting | Parameter Format | Valid Values | Description |
| ----------------------- | -------------------------------------------------------------------------- | ------------ | ------------------------------------------------------- |
| Cooling temperature | `{"set_point_cooling": {"val": <temp>}, "set_point_heating": {"val": 65}}` | 4-10°C | Set target cooling temperature (heating is always 65°C) |
| Cooling temperature | `{"set_point_cooling": {"val": <temp>}}` | 4-10°C | Set target cooling temperature |
| Water hardness | `{"wtr_hardness": {"val": <level>}}` | 1-9 | Set water hardness level |
| Still water calibration | `{"calib_still_wtr": {"val": <amount>}}` | 1-10 | Calibrate still water flow |
| Soda water calibration | `{"calib_soda_wtr": {"val": <amount>}}` | 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": <temp>}}` | 60-100°C | Set target heating temperature |

## Request/Response Examples

Expand Down Expand Up @@ -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`

Expand All @@ -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`

Expand Down Expand Up @@ -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": <temp>}}`

**Parameters:**

- `heating_celsius`: Temperature in Celsius (60-100)

**Returns:** True if successful

#### `set_water_hardness(level: int) -> bool`

Set water hardness level.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
75 changes: 71 additions & 4 deletions custom_components/blanco_unit/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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
)
48 changes: 45 additions & 3 deletions custom_components/blanco_unit/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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).

Expand Down
Loading
Loading