From 43771dad4e2ca47660538df6239b51e561a0137b Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:12:42 +0100 Subject: [PATCH 1/7] add service to scan protocol --- custom_components/blanco_unit/client.py | 53 +++++++ custom_components/blanco_unit/const.py | 5 + custom_components/blanco_unit/services.py | 165 ++++++++++++++++++++ custom_components/blanco_unit/services.yaml | 53 +++++++ test_cli.py | 97 ++++++++++++ 5 files changed, 373 insertions(+) diff --git a/custom_components/blanco_unit/client.py b/custom_components/blanco_unit/client.py index 0030cc1..4cad9f5 100644 --- a/custom_components/blanco_unit/client.py +++ b/custom_components/blanco_unit/client.py @@ -703,6 +703,59 @@ async def set_calibration_soda(self, amount: int) -> bool: resp = await self._execute_transaction(evt_type=7, ctrl=5, pars=req.to_pars()) return resp.get("type") == 2 + # ------------------------------- + # region Protocol Discovery + # ------------------------------- + + async def test_protocol_parameters( + self, evt_type: int, ctrl: int | None = None, pars_evt_type: int | None = None + ) -> dict[str, Any] | None: + """Test protocol parameters and return response if it contains meaningful data. + + Args: + evt_type: Event type to test. + ctrl: Control parameter to test (optional). + pars_evt_type: Parameters event type to test (optional). + + Returns: + Response dictionary if it contains meaningful data, None otherwise. + """ + try: + pars = {"evt_type": pars_evt_type} if pars_evt_type is not None else None + response = await self._execute_transaction( + evt_type=evt_type, ctrl=ctrl, pars=pars + ) + + # Check if response contains meaningful data + if self._is_response_empty(response): + return None + + return response # noqa: TRY300 + except Exception as e: # noqa: BLE001 + _LOGGER.debug( + "Test failed for evt_type=%s, ctrl=%s, pars_evt_type=%s: %s", + evt_type, + ctrl, + pars_evt_type, + e, + ) + return None + + def _is_response_empty(self, response: dict[str, Any]) -> bool: + """Check if a response contains meaningful data. + + Args: + response: Response dictionary to check. + + Returns: + True if response is empty or contains only empty structures. + """ + if not response: + return True + + # Check if body exists + body = response.get("body", {}) + return not body # ------------------------------- # region Standalone Functions diff --git a/custom_components/blanco_unit/const.py b/custom_components/blanco_unit/const.py index dc31bdf..e3d5f7c 100644 --- a/custom_components/blanco_unit/const.py +++ b/custom_components/blanco_unit/const.py @@ -18,6 +18,7 @@ # Service Names HA_SERVICE_DISPENSE_WATER = "dispense_water" HA_SERVICE_CHANGE_PIN = "change_pin" +HA_SERVICE_SCAN_PROTOCOL = "scan_protocol_parameters" # Service Attributes HA_SERVICE_ATTR_DEVICE_ID = "device_id" @@ -25,3 +26,7 @@ HA_SERVICE_ATTR_CO2_INTENSITY = "co2_intensity" HA_SERVICE_ATTR_NEW_PIN = "new_pin" HA_SERVICE_ATTR_UPDATE_CONFIG = "update_config" +HA_SERVICE_ATTR_EVT_TYPE_MAX = "evt_type_max" +HA_SERVICE_ATTR_CTRL_MAX = "ctrl_max" +HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX = "pars_evt_type_max" +HA_SERVICE_ATTR_SAVE_TO_FILE = "save_to_file" diff --git a/custom_components/blanco_unit/services.py b/custom_components/blanco_unit/services.py index 76a7caa..663a51d 100644 --- a/custom_components/blanco_unit/services.py +++ b/custom_components/blanco_unit/services.py @@ -13,11 +13,16 @@ DOMAIN, HA_SERVICE_ATTR_AMOUNT_ML, HA_SERVICE_ATTR_CO2_INTENSITY, + HA_SERVICE_ATTR_CTRL_MAX, HA_SERVICE_ATTR_DEVICE_ID, + HA_SERVICE_ATTR_EVT_TYPE_MAX, HA_SERVICE_ATTR_NEW_PIN, + HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX, + HA_SERVICE_ATTR_SAVE_TO_FILE, HA_SERVICE_ATTR_UPDATE_CONFIG, HA_SERVICE_CHANGE_PIN, HA_SERVICE_DISPENSE_WATER, + HA_SERVICE_SCAN_PROTOCOL, ) from .coordinator import BlancoUnitCoordinator @@ -57,6 +62,22 @@ def _validate_amount_ml(value: int) -> int: } ) +SERVICE_SCAN_PROTOCOL_SCHEMA = vol.Schema( + { + vol.Required(HA_SERVICE_ATTR_DEVICE_ID): cv.string, + vol.Optional(HA_SERVICE_ATTR_EVT_TYPE_MAX, default=10): vol.All( + vol.Coerce(int), vol.Range(min=0, max=255) + ), + vol.Optional(HA_SERVICE_ATTR_CTRL_MAX, default=10): vol.All( + vol.Coerce(int), vol.Range(min=0, max=255) + ), + vol.Optional(HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX, default=10): vol.All( + vol.Coerce(int), vol.Range(min=0, max=255) + ), + vol.Optional(HA_SERVICE_ATTR_SAVE_TO_FILE, default=False): cv.boolean, + } +) + def async_setup_services(hass: HomeAssistant) -> None: """Set up Blanco Unit integration services.""" @@ -104,6 +125,143 @@ async def handle_change_pin(call: ServiceCall) -> None: # Reload the config entry to reconnect with new PIN await hass.config_entries.async_reload(entry_id) + async def handle_scan_protocol(call: ServiceCall) -> None: + """Handle the scan_protocol_parameters service call.""" + import json + from datetime import datetime + + _LOGGER.debug("Scan protocol service called with data: %s", call.data) + coordinator = _get_coordinator(hass, call) + + evt_type_max = call.data[HA_SERVICE_ATTR_EVT_TYPE_MAX] + ctrl_max = call.data[HA_SERVICE_ATTR_CTRL_MAX] + pars_evt_type_max = call.data[HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX] + save_to_file = call.data[HA_SERVICE_ATTR_SAVE_TO_FILE] + + _LOGGER.info( + "Starting protocol scan: evt_type=0-%d, ctrl=0-%d, pars_evt_type=0-%d", + evt_type_max, + ctrl_max, + pars_evt_type_max, + ) + + results = [] + total_tests = 0 + successful_tests = 0 + + # Scan evt_type values + for evt_type in range(evt_type_max + 1): + # Test without ctrl + total_tests += 1 + response = await coordinator.client.test_protocol_parameters( + evt_type, None, None + ) + if response: + successful_tests += 1 + results.append({ + "evt_type": evt_type, + "ctrl": None, + "pars_evt_type": None, + "response": response, + }) + _LOGGER.info( + "✓ Found data: evt_type=%d, ctrl=None, pars_evt_type=None", + evt_type, + ) + + # Test with ctrl values + for ctrl in range(ctrl_max + 1): + # Without pars evt_type + total_tests += 1 + response = await coordinator.client.test_protocol_parameters( + evt_type, ctrl, None + ) + if response: + successful_tests += 1 + results.append({ + "evt_type": evt_type, + "ctrl": ctrl, + "pars_evt_type": None, + "response": response, + }) + _LOGGER.info( + "✓ Found data: evt_type=%d, ctrl=%d, pars_evt_type=None", + evt_type, + ctrl, + ) + + # With pars evt_type values + for pars_evt_type in range(pars_evt_type_max + 1): + total_tests += 1 + response = await coordinator.client.test_protocol_parameters( + evt_type, ctrl, pars_evt_type + ) + if response: + successful_tests += 1 + results.append({ + "evt_type": evt_type, + "ctrl": ctrl, + "pars_evt_type": pars_evt_type, + "response": response, + }) + _LOGGER.info( + "✓ Found data: evt_type=%d, ctrl=%d, pars_evt_type=%d", + evt_type, + ctrl, + pars_evt_type, + ) + + _LOGGER.info( + "Protocol scan complete: %d/%d tests returned data", successful_tests, total_tests + ) + + # Log all results in formatted JSON + if results: + _LOGGER.info("=" * 70) + _LOGGER.info("PROTOCOL SCAN RESULTS") + _LOGGER.info("=" * 70) + for i, result in enumerate(results, 1): + _LOGGER.info( + "Result %d: evt_type=%s, ctrl=%s, pars_evt_type=%s", + i, + result["evt_type"], + result["ctrl"], + result["pars_evt_type"], + ) + _LOGGER.info("Response: %s", json.dumps(result["response"], indent=2)) + _LOGGER.info("=" * 70) + else: + _LOGGER.warning("No meaningful data found in protocol scan") + + # Save to file if requested + if save_to_file and results: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"protocol_scan_{timestamp}.json" + filepath = hass.config.path(filename) + + try: + with open(filepath, "w") as f: + json.dump( + { + "timestamp": timestamp, + "scan_parameters": { + "evt_type_max": evt_type_max, + "ctrl_max": ctrl_max, + "pars_evt_type_max": pars_evt_type_max, + }, + "summary": { + "total_tests": total_tests, + "successful_tests": successful_tests, + }, + "results": results, + }, + f, + indent=2, + ) + _LOGGER.info("Protocol scan results saved to: %s", filepath) + except Exception as e: # noqa: BLE001 + _LOGGER.error("Failed to save protocol scan results: %s", e) + hass.services.async_register( DOMAIN, HA_SERVICE_DISPENSE_WATER, @@ -118,6 +276,13 @@ async def handle_change_pin(call: ServiceCall) -> None: schema=SERVICE_CHANGE_PIN_SCHEMA, ) + hass.services.async_register( + DOMAIN, + HA_SERVICE_SCAN_PROTOCOL, + handle_scan_protocol, + schema=SERVICE_SCAN_PROTOCOL_SCHEMA, + ) + def _get_coordinator(hass: HomeAssistant, call: ServiceCall) -> BlancoUnitCoordinator: """Extract device_id from service call and return the coordinator.""" diff --git a/custom_components/blanco_unit/services.yaml b/custom_components/blanco_unit/services.yaml index e4f18d7..b4d40d6 100644 --- a/custom_components/blanco_unit/services.yaml +++ b/custom_components/blanco_unit/services.yaml @@ -65,3 +65,56 @@ change_pin: default: false selector: boolean: + +scan_protocol_parameters: + name: Scan Protocol Parameters + description: Scan for valid protocol parameters (evt_type, ctrl, pars evt_type). Results are logged to Home Assistant debug log and optionally saved to a file. This is a diagnostic tool for discovering supported device commands. + fields: + device_id: + name: Device + description: The Blanco Unit device to scan. + required: true + example: "a1b2c3d4e5f6" + selector: + device: + integration: blanco_unit + evt_type_max: + name: Max evt_type + description: Maximum evt_type value to test (0 to this value). + required: false + default: 10 + example: 10 + selector: + number: + min: 0 + max: 255 + mode: box + ctrl_max: + name: Max ctrl + description: Maximum ctrl value to test (0 to this value). + required: false + default: 10 + example: 10 + selector: + number: + min: 0 + max: 255 + mode: box + pars_evt_type_max: + name: Max pars evt_type + description: Maximum pars evt_type value to test (0 to this value). + required: false + default: 10 + example: 10 + selector: + number: + min: 0 + max: 255 + mode: box + save_to_file: + name: Save to file + description: If enabled, saves the scan results to a JSON file in the Home Assistant config directory (protocol_scan_TIMESTAMP.json). + required: false + default: false + selector: + boolean: diff --git a/test_cli.py b/test_cli.py index ba078d1..516a737 100755 --- a/test_cli.py +++ b/test_cli.py @@ -236,6 +236,9 @@ async def show_menu(self) -> None: print(" 10. Set Soda Water Calibration") print(" 11. Change PIN") print() + print("Testing:") + print(" 12. Scan Protocol Parameters") + print() print("Other:") print(" 0. Disconnect and Exit") print("=" * 70) @@ -267,6 +270,8 @@ async def show_menu(self) -> None: await self.test_set_calibration_soda() elif choice == "11": await self.test_change_pin() + elif choice == "12": + await self.test_scan_protocol_parameters() else: print("Invalid option. Please try again.") except Exception as e: @@ -470,6 +475,98 @@ async def test_change_pin(self) -> None: else: print("PIN must be exactly 5 digits. Please try again.") + async def test_scan_protocol_parameters(self) -> None: + """Scan for valid protocol parameters.""" + print("\n--- Scan Protocol Parameters ---") + print("This will test evt_type 0-10, ctrl 0-10, and pars evt_type 0-10") + print("Only responses with meaningful data will be shown.") + print() + + confirm = input("This may take a few minutes. Continue? (y/n): ").strip().lower() + if confirm != 'y': + print("Scan cancelled") + return + + results = [] + total_tests = 0 + successful_tests = 0 + + print("\nScanning...") + + # Test evt_type 0-10 + for evt_type in range(11): + # Test without ctrl + print(f" Testing evt_type={evt_type}, ctrl=None, pars_evt_type=None...") + total_tests += 1 + response = await self.client.test_protocol_parameters(evt_type, None, None) + if response: + successful_tests += 1 + results.append({ + "evt_type": evt_type, + "ctrl": None, + "pars_evt_type": None, + "response": response + }) + + # Test with ctrl 0-10 + for ctrl in range(11): + # Without pars evt_type + print(f" Testing evt_type={evt_type}, ctrl={ctrl}, pars_evt_type=None...") + total_tests += 1 + response = await self.client.test_protocol_parameters(evt_type, ctrl, None) + if response: + successful_tests += 1 + results.append({ + "evt_type": evt_type, + "ctrl": ctrl, + "pars_evt_type": None, + "response": response + }) + + # With pars evt_type 0-10 + for pars_evt_type in range(11): + print(f" Testing evt_type={evt_type}, ctrl={ctrl}, pars_evt_type={pars_evt_type}...") + total_tests += 1 + response = await self.client.test_protocol_parameters(evt_type, ctrl, pars_evt_type) + if response: + successful_tests += 1 + results.append({ + "evt_type": evt_type, + "ctrl": ctrl, + "pars_evt_type": pars_evt_type, + "response": response + }) + + print("\n" + "=" * 70) + print(f"Scan Complete: {successful_tests}/{total_tests} tests returned data") + print("=" * 70) + + if results: + print("\nResults with meaningful data:\n") + for i, result in enumerate(results, 1): + print(f"\n--- Result {i} ---") + print(f"evt_type: {result['evt_type']}") + print(f"ctrl: {result['ctrl']}") + print(f"pars_evt_type: {result['pars_evt_type']}") + print(f"Response:") + import json + print(json.dumps(result['response'], indent=2)) + else: + print("\nNo meaningful data found in any test.") + + # Offer to save results + if results: + save = input("\nSave results to file? (y/n): ").strip().lower() + if save == 'y': + filename = input("Enter filename (default: protocol_scan_results.json): ").strip() + if not filename: + filename = "protocol_scan_results.json" + + import json + with open(filename, 'w') as f: + json.dump(results, f, indent=2) + print(f"✓ Results saved to {filename}") + async def cleanup(self) -> None: """Clean up resources.""" if self.client: From bf7ef0f5a9f4962ceaec703bde1c6258d1316572 Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:37:45 +0100 Subject: [PATCH 2/7] fix unnecessary duplicate connect --- custom_components/blanco_unit/client.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/custom_components/blanco_unit/client.py b/custom_components/blanco_unit/client.py index 4cad9f5..7179c8e 100644 --- a/custom_components/blanco_unit/client.py +++ b/custom_components/blanco_unit/client.py @@ -519,9 +519,8 @@ async def _execute_transaction( async def get_system_info(self) -> BlancoUnitSystemInfo: """Read and return system information (firmware versions, device name, reset count).""" - session_data = await self._connect() resp = await self._execute_transaction(evt_type=7, ctrl=3, pars={"evt_type": 2}) - pars = session_data.protocol.extract_pars(resp) + pars = self._session_data.protocol.extract_pars(resp) return BlancoUnitSystemInfo( sw_ver_comm_con=pars.get("sw_ver_comm_con", {}).get("val", "Unknown"), sw_ver_elec_con=pars.get("sw_ver_elec_con", {}).get("val", "Unknown"), @@ -532,9 +531,8 @@ async def get_system_info(self) -> BlancoUnitSystemInfo: async def get_settings(self) -> BlancoUnitSettings: """Read and return device configuration settings.""" - session_data = await self._connect() resp = await self._execute_transaction(evt_type=7, ctrl=3, pars={"evt_type": 5}) - pars = session_data.protocol.extract_pars(resp) + pars = self._session_data.protocol.extract_pars(resp) return BlancoUnitSettings( calib_still_wtr=pars.get("calib_still_wtr", {}).get("val", 0), calib_soda_wtr=pars.get("calib_soda_wtr", {}).get("val", 0), @@ -546,9 +544,8 @@ async def get_settings(self) -> BlancoUnitSettings: async def get_status(self) -> BlancoUnitStatus: """Read and return real-time device status.""" - session_data = await self._connect() resp = await self._execute_transaction(evt_type=7, ctrl=3, pars={"evt_type": 6}) - pars = session_data.protocol.extract_pars(resp) + pars = self._session_data.protocol.extract_pars(resp) return BlancoUnitStatus( tap_state=pars.get("tap_state", {}).get("val", 0), filter_rest=pars.get("filter_rest", {}).get("val", 0), @@ -562,9 +559,8 @@ async def get_status(self) -> BlancoUnitStatus: async def get_device_identity(self) -> BlancoUnitIdentity: """Read and return device identity (serial number, service code).""" - session_data = await self._connect() resp = await self._execute_transaction(evt_type=7, ctrl=2, pars={}) - pars = session_data.protocol.extract_pars(resp) + pars = self._session_data.protocol.extract_pars(resp) return BlancoUnitIdentity( serial_no=pars.get("ser_no", "Unknown"), service_code=pars.get("serv_code", "Unknown"), @@ -572,9 +568,8 @@ async def get_device_identity(self) -> BlancoUnitIdentity: async def get_wifi_info(self) -> BlancoUnitWifiInfo: """Read and return WiFi and network information.""" - session_data = await self._connect() resp = await self._execute_transaction(evt_type=7, ctrl=10, pars={}) - pars = session_data.protocol.extract_pars(resp) + pars = self._session_data.protocol.extract_pars(resp) return BlancoUnitWifiInfo( cloud_connect=pars.get("cloud_connect", {}).get("val", False), ssid=pars.get("ssid", {}).get("val", ""), From 638d91c27409fa830cb8ffb19e6c8563c49f5d8c Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:11:18 +0100 Subject: [PATCH 3/7] add service to test specific values --- .coverage | Bin 53248 -> 53248 bytes custom_components/blanco_unit/config_flow.py | 3 - custom_components/blanco_unit/const.py | 7 +- custom_components/blanco_unit/coordinator.py | 12 ++ custom_components/blanco_unit/services.py | 177 +++++-------------- custom_components/blanco_unit/services.yaml | 40 ++--- test_cli.py | 169 +++++------------- 7 files changed, 118 insertions(+), 290 deletions(-) diff --git a/.coverage b/.coverage index 926ab5b48c4a4e55b328e21c45e8f1c19b7e0ac6..37557c2dc345a40d29eb2aacb3e18199e0162928 100644 GIT binary patch delta 280 zcmV+z0q6dJpaX!Q1F!~wASnP3`48L=(GRB&H4h{X{|@sG+z!nS!Va(wo(@nBDYFp} z0}c}<4+H@TOb_bagXLz`oAZ4Bd_I#-j>CT=2Lu5LNC#RM04SZm;V-@o%Ju5WzpDTM z0001h&*w*|#qs^?pyB}?4z|aHstsQ86AL(0nz+ z57@VMp!I#EZ`}ds_4?xz^q>Bx|EHh+KM(+W0q_FgF97^r0bYOy1o-Cw09YoUH{cE? n1Ox#IPLr*V7!~M$yZ2uA{eEv32?GQH2^0e=pP3J{+>azcNCRV5 diff --git a/custom_components/blanco_unit/config_flow.py b/custom_components/blanco_unit/config_flow.py index 7cea60f..adadb73 100644 --- a/custom_components/blanco_unit/config_flow.py +++ b/custom_components/blanco_unit/config_flow.py @@ -15,9 +15,6 @@ from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.selector import ( - NumberSelector, - NumberSelectorConfig, - NumberSelectorMode, TextSelector, TextSelectorConfig, TextSelectorType, diff --git a/custom_components/blanco_unit/const.py b/custom_components/blanco_unit/const.py index e3d5f7c..b31a312 100644 --- a/custom_components/blanco_unit/const.py +++ b/custom_components/blanco_unit/const.py @@ -26,7 +26,6 @@ HA_SERVICE_ATTR_CO2_INTENSITY = "co2_intensity" HA_SERVICE_ATTR_NEW_PIN = "new_pin" HA_SERVICE_ATTR_UPDATE_CONFIG = "update_config" -HA_SERVICE_ATTR_EVT_TYPE_MAX = "evt_type_max" -HA_SERVICE_ATTR_CTRL_MAX = "ctrl_max" -HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX = "pars_evt_type_max" -HA_SERVICE_ATTR_SAVE_TO_FILE = "save_to_file" +HA_SERVICE_ATTR_EVT_TYPE = "evt_type" +HA_SERVICE_ATTR_CTRL = "ctrl" +HA_SERVICE_ATTR_PARS_EVT_TYPE = "pars_evt_type" diff --git a/custom_components/blanco_unit/coordinator.py b/custom_components/blanco_unit/coordinator.py index 5085099..5dde347 100644 --- a/custom_components/blanco_unit/coordinator.py +++ b/custom_components/blanco_unit/coordinator.py @@ -180,6 +180,18 @@ def _connection_changed(self, connected: bool) -> None: if self.data is not None: self.async_set_updated_data(replace(self.data, connected=connected)) + # ------------------------------- + # region Testing + # ------------------------------- + + async def test_protocol_parameters( + self, evt_type: int, ctrl: int | None = None, pars_evt_type: int | None = None + ) -> dict[str, Any] | None: + """Test protocol parameters by sending a custom event.""" + return await self._call( + self._client.test_protocol_parameters, evt_type, ctrl, pars_evt_type + ) + # ------------------------------- # region internal # ------------------------------- diff --git a/custom_components/blanco_unit/services.py b/custom_components/blanco_unit/services.py index 663a51d..59debb5 100644 --- a/custom_components/blanco_unit/services.py +++ b/custom_components/blanco_unit/services.py @@ -1,10 +1,11 @@ """Home Assistant services provided by the Blanco Unit integration.""" +import json import logging import voluptuous as vol -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -13,12 +14,11 @@ DOMAIN, HA_SERVICE_ATTR_AMOUNT_ML, HA_SERVICE_ATTR_CO2_INTENSITY, - HA_SERVICE_ATTR_CTRL_MAX, + HA_SERVICE_ATTR_CTRL, HA_SERVICE_ATTR_DEVICE_ID, - HA_SERVICE_ATTR_EVT_TYPE_MAX, + HA_SERVICE_ATTR_EVT_TYPE, HA_SERVICE_ATTR_NEW_PIN, - HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX, - HA_SERVICE_ATTR_SAVE_TO_FILE, + HA_SERVICE_ATTR_PARS_EVT_TYPE, HA_SERVICE_ATTR_UPDATE_CONFIG, HA_SERVICE_CHANGE_PIN, HA_SERVICE_DISPENSE_WATER, @@ -65,16 +65,15 @@ def _validate_amount_ml(value: int) -> int: SERVICE_SCAN_PROTOCOL_SCHEMA = vol.Schema( { vol.Required(HA_SERVICE_ATTR_DEVICE_ID): cv.string, - vol.Optional(HA_SERVICE_ATTR_EVT_TYPE_MAX, default=10): vol.All( + vol.Required(HA_SERVICE_ATTR_EVT_TYPE): vol.All( vol.Coerce(int), vol.Range(min=0, max=255) ), - vol.Optional(HA_SERVICE_ATTR_CTRL_MAX, default=10): vol.All( + vol.Optional(HA_SERVICE_ATTR_CTRL): vol.All( vol.Coerce(int), vol.Range(min=0, max=255) ), - vol.Optional(HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX, default=10): vol.All( + vol.Optional(HA_SERVICE_ATTR_PARS_EVT_TYPE): vol.All( vol.Coerce(int), vol.Range(min=0, max=255) ), - vol.Optional(HA_SERVICE_ATTR_SAVE_TO_FILE, default=False): cv.boolean, } ) @@ -125,142 +124,53 @@ async def handle_change_pin(call: ServiceCall) -> None: # Reload the config entry to reconnect with new PIN await hass.config_entries.async_reload(entry_id) - async def handle_scan_protocol(call: ServiceCall) -> None: + async def handle_scan_protocol(call: ServiceCall) -> dict: """Handle the scan_protocol_parameters service call.""" - import json - from datetime import datetime _LOGGER.debug("Scan protocol service called with data: %s", call.data) coordinator = _get_coordinator(hass, call) - evt_type_max = call.data[HA_SERVICE_ATTR_EVT_TYPE_MAX] - ctrl_max = call.data[HA_SERVICE_ATTR_CTRL_MAX] - pars_evt_type_max = call.data[HA_SERVICE_ATTR_PARS_EVT_TYPE_MAX] - save_to_file = call.data[HA_SERVICE_ATTR_SAVE_TO_FILE] + evt_type = call.data[HA_SERVICE_ATTR_EVT_TYPE] + ctrl = call.data.get(HA_SERVICE_ATTR_CTRL) + pars_evt_type = call.data.get(HA_SERVICE_ATTR_PARS_EVT_TYPE) _LOGGER.info( - "Starting protocol scan: evt_type=0-%d, ctrl=0-%d, pars_evt_type=0-%d", - evt_type_max, - ctrl_max, - pars_evt_type_max, + "Testing protocol parameters: evt_type=%d, ctrl=%s, pars_evt_type=%s", + evt_type, + ctrl, + pars_evt_type, ) - results = [] - total_tests = 0 - successful_tests = 0 - - # Scan evt_type values - for evt_type in range(evt_type_max + 1): - # Test without ctrl - total_tests += 1 - response = await coordinator.client.test_protocol_parameters( - evt_type, None, None - ) - if response: - successful_tests += 1 - results.append({ - "evt_type": evt_type, - "ctrl": None, - "pars_evt_type": None, - "response": response, - }) - _LOGGER.info( - "✓ Found data: evt_type=%d, ctrl=None, pars_evt_type=None", - evt_type, - ) - - # Test with ctrl values - for ctrl in range(ctrl_max + 1): - # Without pars evt_type - total_tests += 1 - response = await coordinator.client.test_protocol_parameters( - evt_type, ctrl, None - ) - if response: - successful_tests += 1 - results.append({ - "evt_type": evt_type, - "ctrl": ctrl, - "pars_evt_type": None, - "response": response, - }) - _LOGGER.info( - "✓ Found data: evt_type=%d, ctrl=%d, pars_evt_type=None", - evt_type, - ctrl, - ) - - # With pars evt_type values - for pars_evt_type in range(pars_evt_type_max + 1): - total_tests += 1 - response = await coordinator.client.test_protocol_parameters( - evt_type, ctrl, pars_evt_type - ) - if response: - successful_tests += 1 - results.append({ - "evt_type": evt_type, - "ctrl": ctrl, - "pars_evt_type": pars_evt_type, - "response": response, - }) - _LOGGER.info( - "✓ Found data: evt_type=%d, ctrl=%d, pars_evt_type=%d", - evt_type, - ctrl, - pars_evt_type, - ) - - _LOGGER.info( - "Protocol scan complete: %d/%d tests returned data", successful_tests, total_tests + # Test the specific parameter combination + response = await coordinator.test_protocol_parameters( + evt_type, ctrl, pars_evt_type ) - # Log all results in formatted JSON - if results: - _LOGGER.info("=" * 70) - _LOGGER.info("PROTOCOL SCAN RESULTS") - _LOGGER.info("=" * 70) - for i, result in enumerate(results, 1): - _LOGGER.info( - "Result %d: evt_type=%s, ctrl=%s, pars_evt_type=%s", - i, - result["evt_type"], - result["ctrl"], - result["pars_evt_type"], - ) - _LOGGER.info("Response: %s", json.dumps(result["response"], indent=2)) - _LOGGER.info("=" * 70) + result = { + "evt_type": evt_type, + "ctrl": ctrl, + "pars_evt_type": pars_evt_type, + "success": response is not None, + "response": response if response else None, + } + + if response: + _LOGGER.info( + "✓ Found data: evt_type=%d, ctrl=%s, pars_evt_type=%s", + evt_type, + ctrl, + pars_evt_type, + ) + _LOGGER.info("Response: %s", json.dumps(response, indent=2)) else: - _LOGGER.warning("No meaningful data found in protocol scan") - - # Save to file if requested - if save_to_file and results: - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"protocol_scan_{timestamp}.json" - filepath = hass.config.path(filename) - - try: - with open(filepath, "w") as f: - json.dump( - { - "timestamp": timestamp, - "scan_parameters": { - "evt_type_max": evt_type_max, - "ctrl_max": ctrl_max, - "pars_evt_type_max": pars_evt_type_max, - }, - "summary": { - "total_tests": total_tests, - "successful_tests": successful_tests, - }, - "results": results, - }, - f, - indent=2, - ) - _LOGGER.info("Protocol scan results saved to: %s", filepath) - except Exception as e: # noqa: BLE001 - _LOGGER.error("Failed to save protocol scan results: %s", e) + _LOGGER.warning( + "✗ No data: evt_type=%d, ctrl=%s, pars_evt_type=%s", + evt_type, + ctrl, + pars_evt_type, + ) + + return result hass.services.async_register( DOMAIN, @@ -281,6 +191,7 @@ async def handle_scan_protocol(call: ServiceCall) -> None: HA_SERVICE_SCAN_PROTOCOL, handle_scan_protocol, schema=SERVICE_SCAN_PROTOCOL_SCHEMA, + supports_response=SupportsResponse.ONLY, ) diff --git a/custom_components/blanco_unit/services.yaml b/custom_components/blanco_unit/services.yaml index b4d40d6..cdf4a08 100644 --- a/custom_components/blanco_unit/services.yaml +++ b/custom_components/blanco_unit/services.yaml @@ -68,53 +68,43 @@ change_pin: scan_protocol_parameters: name: Scan Protocol Parameters - description: Scan for valid protocol parameters (evt_type, ctrl, pars evt_type). Results are logged to Home Assistant debug log and optionally saved to a file. This is a diagnostic tool for discovering supported device commands. + description: Test a specific combination of protocol parameters (evt_type, ctrl, pars_evt_type). Results are returned and also logged to Home Assistant debug log. This is a diagnostic tool for discovering supported device commands. fields: device_id: name: Device - description: The Blanco Unit device to scan. + description: The Blanco Unit device to test. required: true example: "a1b2c3d4e5f6" selector: device: integration: blanco_unit - evt_type_max: - name: Max evt_type - description: Maximum evt_type value to test (0 to this value). - required: false - default: 10 - example: 10 + evt_type: + name: Event Type + description: The evt_type value to test. + required: true + example: 5 selector: number: min: 0 max: 255 mode: box - ctrl_max: - name: Max ctrl - description: Maximum ctrl value to test (0 to this value). + ctrl: + name: Control + description: The ctrl value to test (optional, leave empty to test without ctrl). required: false - default: 10 - example: 10 + example: 1 selector: number: min: 0 max: 255 mode: box - pars_evt_type_max: - name: Max pars evt_type - description: Maximum pars evt_type value to test (0 to this value). + pars_evt_type: + name: Parameter Event Type + description: The pars_evt_type value to test (optional, leave empty to test without pars_evt_type). required: false - default: 10 - example: 10 + example: 2 selector: number: min: 0 max: 255 mode: box - save_to_file: - name: Save to file - description: If enabled, saves the scan results to a JSON file in the Home Assistant config directory (protocol_scan_TIMESTAMP.json). - required: false - default: false - selector: - boolean: diff --git a/test_cli.py b/test_cli.py index 516a737..6742a20 100755 --- a/test_cli.py +++ b/test_cli.py @@ -4,18 +4,18 @@ from __future__ import annotations import asyncio + +# Import client with workaround for relative imports +import importlib.util +import os import sys -from typing import Any +import traceback +import types import bleak from bleak import BleakScanner -from bleak.backends.device import BLEDevice from bleak.backends import get_default_backend - - -# Import client with workaround for relative imports -import importlib.util -import os +from bleak.backends.device import BLEDevice # Create a proper package structure in sys.modules to support relative imports sys.path.insert(0, "custom_components") @@ -35,7 +35,8 @@ data_spec.loader.exec_module(data_module) # Create a fake package module for blanco_unit -import types + + blanco_unit_package = types.ModuleType("blanco_unit") blanco_unit_package.__path__ = [os.path.join("custom_components", "blanco_unit")] blanco_unit_package.__package__ = "blanco_unit" @@ -43,8 +44,11 @@ # Load client module with package context client_path = os.path.join("custom_components", "blanco_unit", "client.py") -client_spec = importlib.util.spec_from_file_location("blanco_unit.client", client_path, - submodule_search_locations=[os.path.join("custom_components", "blanco_unit")]) +client_spec = importlib.util.spec_from_file_location( + "blanco_unit.client", + client_path, + submodule_search_locations=[os.path.join("custom_components", "blanco_unit")], +) client_module = importlib.util.module_from_spec(client_spec) client_module.__package__ = "blanco_unit" sys.modules["blanco_unit.client"] = client_module @@ -75,6 +79,7 @@ def print_header(self) -> None: except AttributeError: try: import importlib.metadata + bleak_version = importlib.metadata.version("bleak") except Exception: # noqa: BLE001 bleak_version = "Unknown" @@ -139,7 +144,10 @@ def display_devices(self, devices: list[BLEDevice]) -> None: print(f" Address: {device.address}") # Get RSSI from advertisement data if available - if hasattr(self, 'devices_with_advdata') and device.address in self.devices_with_advdata: + if ( + hasattr(self, "devices_with_advdata") + and device.address in self.devices_with_advdata + ): _, adv_data = self.devices_with_advdata[device.address] rssi = adv_data.rssi print(f" RSSI: {rssi} dBm") @@ -151,7 +159,7 @@ def select_device(self, devices: list[BLEDevice]) -> BLEDevice | None: while True: try: choice = input("Select device number (or 'q' to quit): ").strip() - if choice.lower() == 'q': + if choice.lower() == "q": return None idx = int(choice) - 1 @@ -184,7 +192,9 @@ async def connect_to_device(self) -> bool: print("Error: Device or PIN not set") return False - print(f"\nConnecting to {self.device.name or 'Unknown'} ({self.device.address})...") + print( + f"\nConnecting to {self.device.name or 'Unknown'} ({self.device.address})..." + ) try: self.client = BlancoUnitBluetoothClient( @@ -236,9 +246,6 @@ async def show_menu(self) -> None: print(" 10. Set Soda Water Calibration") print(" 11. Change PIN") print() - print("Testing:") - print(" 12. Scan Protocol Parameters") - print() print("Other:") print(" 0. Disconnect and Exit") print("=" * 70) @@ -248,7 +255,7 @@ async def show_menu(self) -> None: try: if choice == "0": break - elif choice == "1": + if choice == "1": await self.test_get_system_info() elif choice == "2": await self.test_get_settings() @@ -270,13 +277,12 @@ async def show_menu(self) -> None: await self.test_set_calibration_soda() elif choice == "11": await self.test_change_pin() - elif choice == "12": - await self.test_scan_protocol_parameters() else: print("Invalid option. Please try again.") except Exception as e: print(f"\n✗ Error: {e}") import traceback + traceback.print_exc() # Test methods for each function @@ -353,8 +359,7 @@ async def test_set_temperature(self) -> None: else: print("✗ Failed to set temperature") break - else: - print("Temperature must be between 4 and 10°C") + print("Temperature must be between 4 and 10°C") except ValueError: print("Please enter a valid number") @@ -375,8 +380,7 @@ async def test_set_water_hardness(self) -> None: else: print("✗ Failed to set water hardness") break - else: - print("Hardness level must be between 1 and 9") + print("Hardness level must be between 1 and 9") except ValueError: print("Please enter a valid number") @@ -402,11 +406,15 @@ async def test_dispense_water(self) -> None: print("CO2 intensity must be 1, 2, or 3") continue - confirm = input(f"Dispense {amount_int}ml with CO2 level {co2_int}? (y/n): ").strip().lower() - if confirm == 'y': + confirm = ( + input(f"Dispense {amount_int}ml with CO2 level {co2_int}? (y/n): ") + .strip() + .lower() + ) + if confirm == "y": result = await self.client.dispense_water(amount_int, co2_int) if result: - print(f"✓ Dispensing started successfully") + print("✓ Dispensing started successfully") else: print("✗ Failed to start dispensing") break @@ -452,7 +460,9 @@ async def test_change_pin(self) -> None: print("⚠️ WARNING: This will change the device PIN!") print("⚠️ Make sure you remember the new PIN!") - confirm = input("Are you sure you want to change the PIN? (yes/no): ").strip().lower() + confirm = ( + input("Are you sure you want to change the PIN? (yes/no): ").strip().lower() + ) if confirm != "yes": print("PIN change cancelled") return @@ -470,103 +480,10 @@ async def test_change_pin(self) -> None: else: print("✗ Failed to change PIN") break - else: - print("PINs don't match. Please try again.") + print("PINs don't match. Please try again.") else: print("PIN must be exactly 5 digits. Please try again.") - async def test_scan_protocol_parameters(self) -> None: - """Scan for valid protocol parameters.""" - print("\n--- Scan Protocol Parameters ---") - print("This will test evt_type 0-10, ctrl 0-10, and pars evt_type 0-10") - print("Only responses with meaningful data will be shown.") - print() - - confirm = input("This may take a few minutes. Continue? (y/n): ").strip().lower() - if confirm != 'y': - print("Scan cancelled") - return - - results = [] - total_tests = 0 - successful_tests = 0 - - print("\nScanning...") - - # Test evt_type 0-10 - for evt_type in range(11): - # Test without ctrl - print(f" Testing evt_type={evt_type}, ctrl=None, pars_evt_type=None...") - total_tests += 1 - response = await self.client.test_protocol_parameters(evt_type, None, None) - if response: - successful_tests += 1 - results.append({ - "evt_type": evt_type, - "ctrl": None, - "pars_evt_type": None, - "response": response - }) - - # Test with ctrl 0-10 - for ctrl in range(11): - # Without pars evt_type - print(f" Testing evt_type={evt_type}, ctrl={ctrl}, pars_evt_type=None...") - total_tests += 1 - response = await self.client.test_protocol_parameters(evt_type, ctrl, None) - if response: - successful_tests += 1 - results.append({ - "evt_type": evt_type, - "ctrl": ctrl, - "pars_evt_type": None, - "response": response - }) - - # With pars evt_type 0-10 - for pars_evt_type in range(11): - print(f" Testing evt_type={evt_type}, ctrl={ctrl}, pars_evt_type={pars_evt_type}...") - total_tests += 1 - response = await self.client.test_protocol_parameters(evt_type, ctrl, pars_evt_type) - if response: - successful_tests += 1 - results.append({ - "evt_type": evt_type, - "ctrl": ctrl, - "pars_evt_type": pars_evt_type, - "response": response - }) - - print("\n" + "=" * 70) - print(f"Scan Complete: {successful_tests}/{total_tests} tests returned data") - print("=" * 70) - - if results: - print("\nResults with meaningful data:\n") - for i, result in enumerate(results, 1): - print(f"\n--- Result {i} ---") - print(f"evt_type: {result['evt_type']}") - print(f"ctrl: {result['ctrl']}") - print(f"pars_evt_type: {result['pars_evt_type']}") - print(f"Response:") - import json - print(json.dumps(result['response'], indent=2)) - else: - print("\nNo meaningful data found in any test.") - - # Offer to save results - if results: - save = input("\nSave results to file? (y/n): ").strip().lower() - if save == 'y': - filename = input("Enter filename (default: protocol_scan_results.json): ").strip() - if not filename: - filename = "protocol_scan_results.json" - - import json - with open(filename, 'w') as f: - json.dump(results, f, indent=2) - print(f"✓ Results saved to {filename}") - async def cleanup(self) -> None: """Clean up resources.""" if self.client: @@ -594,7 +511,9 @@ async def run(self) -> None: print("No device selected. Exiting.") return - print(f"\nSelected: {self.device.name or 'Unknown'} ({self.device.address})") + print( + f"\nSelected: {self.device.name or 'Unknown'} ({self.device.address})" + ) # Get PIN self.pin = self.get_pin() @@ -606,9 +525,9 @@ async def run(self) -> None: except KeyboardInterrupt: print("\n\nInterrupted by user") - except Exception as e: + except Exception as e: # noqa: BLE001 print(f"\n✗ Unexpected error: {e}") - import traceback + traceback.print_exc() finally: await self.cleanup() @@ -621,4 +540,4 @@ async def main() -> None: if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From 4ec59a125ea6c119d1713ecd95c5062133908f58 Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:34:14 +0100 Subject: [PATCH 4/7] add service to test exact payload --- BLUETOOTH_PROTOCOL.md | 46 +++++++++++++------- custom_components/blanco_unit/client.py | 9 ++-- custom_components/blanco_unit/const.py | 4 +- custom_components/blanco_unit/coordinator.py | 4 +- custom_components/blanco_unit/services.py | 38 +++++++--------- custom_components/blanco_unit/services.yaml | 44 +++++++------------ 6 files changed, 67 insertions(+), 78 deletions(-) diff --git a/BLUETOOTH_PROTOCOL.md b/BLUETOOTH_PROTOCOL.md index 085e16d..8a29f59 100644 --- a/BLUETOOTH_PROTOCOL.md +++ b/BLUETOOTH_PROTOCOL.md @@ -254,25 +254,39 @@ Example: ## Event Types -| Event Type | Control Code | Description | -| ---------- | ------------ | ---------------------------------------------------- | -| 7 | 1 | Subscribe to events | -| 7 | 2 | Get device identity (serial number, service code) | -| 7 | 3 | Get device information (requires `evt_type` in pars) | -| 7 | 5 | Set device settings | -| 7 | 10 | Get WiFi information | -| 7 | 13 | Change PIN | -| 7 | 1000 | Dispense water | -| 10 | N/A | Initial pairing/authentication | +### Main Event Types + +| Event Type | Control Code | Description | Parameters (`pars`) | +| ---------- | ------------ | ------------------------------------- | ------------------------------------------------------------------------------------ | +| 7 | 2 | Get device identity | `{}` - Returns serial number and service code | +| 7 | 3 | Get device information | `{"evt_type": }` - See sub-event types below | +| 7 | 5 | Set device settings | See settings parameters below | +| 7 | 10 | Get WiFi information | `{}` - Returns WiFi SSID, signal, IP, MAC addresses, gateway, subnet | +| 7 | 13 | Change PIN | `{"new_pass": "<5_digit_pin>"}` - Changes device PIN (string) | +| 7 | 1000 | Dispense water | `{"disp_amt": , "co2_int": }` - Amount: 100-1500ml (multiples of 100), Intensity: 1=still, 2=medium, 3=high | +| 10 | N/A | Initial pairing/authentication | `{}` - Returns device ID on successful authentication | ### Sub-Event Types (evt_type in pars for ctrl=3) -| evt_type | Description | -| -------- | --------------------------------------------------------------------- | -| 2 | System information (firmware versions, device name, reset count) | -| 4 | Error information | -| 5 | Device settings (calibration, filter lifetime, temperature, hardness) | -| 6 | Device status (filter/CO2 remaining, tap state, errors) | +When using event type 7 with control code 3, you must specify a sub-event type in the `pars` field: + +| evt_type | Description | Returns | +| -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------ | +| 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 | + +### 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": }, "set_point_heating": {"val": 65}}` | 4-10°C | Set target cooling temperature (heating is always 65°C) | +| 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| ## Request/Response Examples diff --git a/custom_components/blanco_unit/client.py b/custom_components/blanco_unit/client.py index 7179c8e..774831e 100644 --- a/custom_components/blanco_unit/client.py +++ b/custom_components/blanco_unit/client.py @@ -703,20 +703,19 @@ async def set_calibration_soda(self, amount: int) -> bool: # ------------------------------- async def test_protocol_parameters( - self, evt_type: int, ctrl: int | None = None, pars_evt_type: int | None = None + self, evt_type: int, ctrl: int | None = None, pars: dict[str, Any] | None = None ) -> dict[str, Any] | None: """Test protocol parameters and return response if it contains meaningful data. Args: evt_type: Event type to test. ctrl: Control parameter to test (optional). - pars_evt_type: Parameters event type to test (optional). + pars: Parameters dictionary to test (optional). Returns: Response dictionary if it contains meaningful data, None otherwise. """ try: - pars = {"evt_type": pars_evt_type} if pars_evt_type is not None else None response = await self._execute_transaction( evt_type=evt_type, ctrl=ctrl, pars=pars ) @@ -728,10 +727,10 @@ async def test_protocol_parameters( return response # noqa: TRY300 except Exception as e: # noqa: BLE001 _LOGGER.debug( - "Test failed for evt_type=%s, ctrl=%s, pars_evt_type=%s: %s", + "Test failed for evt_type=%s, ctrl=%s, pars=%s: %s", evt_type, ctrl, - pars_evt_type, + pars, e, ) return None diff --git a/custom_components/blanco_unit/const.py b/custom_components/blanco_unit/const.py index b31a312..d1f0d74 100644 --- a/custom_components/blanco_unit/const.py +++ b/custom_components/blanco_unit/const.py @@ -26,6 +26,4 @@ HA_SERVICE_ATTR_CO2_INTENSITY = "co2_intensity" HA_SERVICE_ATTR_NEW_PIN = "new_pin" HA_SERVICE_ATTR_UPDATE_CONFIG = "update_config" -HA_SERVICE_ATTR_EVT_TYPE = "evt_type" -HA_SERVICE_ATTR_CTRL = "ctrl" -HA_SERVICE_ATTR_PARS_EVT_TYPE = "pars_evt_type" +HA_SERVICE_ATTR_DATA = "data" diff --git a/custom_components/blanco_unit/coordinator.py b/custom_components/blanco_unit/coordinator.py index 5dde347..2a1d2ab 100644 --- a/custom_components/blanco_unit/coordinator.py +++ b/custom_components/blanco_unit/coordinator.py @@ -185,11 +185,11 @@ def _connection_changed(self, connected: bool) -> None: # ------------------------------- async def test_protocol_parameters( - self, evt_type: int, ctrl: int | None = None, pars_evt_type: int | None = None + self, evt_type: int, ctrl: int | None = None, pars: dict[str, Any] | None = None ) -> dict[str, Any] | None: """Test protocol parameters by sending a custom event.""" return await self._call( - self._client.test_protocol_parameters, evt_type, ctrl, pars_evt_type + self._client.test_protocol_parameters, evt_type, ctrl, pars ) # ------------------------------- diff --git a/custom_components/blanco_unit/services.py b/custom_components/blanco_unit/services.py index 59debb5..05250c4 100644 --- a/custom_components/blanco_unit/services.py +++ b/custom_components/blanco_unit/services.py @@ -14,11 +14,9 @@ DOMAIN, HA_SERVICE_ATTR_AMOUNT_ML, HA_SERVICE_ATTR_CO2_INTENSITY, - HA_SERVICE_ATTR_CTRL, + HA_SERVICE_ATTR_DATA, HA_SERVICE_ATTR_DEVICE_ID, - HA_SERVICE_ATTR_EVT_TYPE, HA_SERVICE_ATTR_NEW_PIN, - HA_SERVICE_ATTR_PARS_EVT_TYPE, HA_SERVICE_ATTR_UPDATE_CONFIG, HA_SERVICE_CHANGE_PIN, HA_SERVICE_DISPENSE_WATER, @@ -65,15 +63,7 @@ def _validate_amount_ml(value: int) -> int: SERVICE_SCAN_PROTOCOL_SCHEMA = vol.Schema( { vol.Required(HA_SERVICE_ATTR_DEVICE_ID): cv.string, - vol.Required(HA_SERVICE_ATTR_EVT_TYPE): vol.All( - vol.Coerce(int), vol.Range(min=0, max=255) - ), - vol.Optional(HA_SERVICE_ATTR_CTRL): vol.All( - vol.Coerce(int), vol.Range(min=0, max=255) - ), - vol.Optional(HA_SERVICE_ATTR_PARS_EVT_TYPE): vol.All( - vol.Coerce(int), vol.Range(min=0, max=255) - ), + vol.Required(HA_SERVICE_ATTR_DATA): dict, } ) @@ -130,44 +120,46 @@ async def handle_scan_protocol(call: ServiceCall) -> dict: _LOGGER.debug("Scan protocol service called with data: %s", call.data) coordinator = _get_coordinator(hass, call) - evt_type = call.data[HA_SERVICE_ATTR_EVT_TYPE] - ctrl = call.data.get(HA_SERVICE_ATTR_CTRL) - pars_evt_type = call.data.get(HA_SERVICE_ATTR_PARS_EVT_TYPE) + # Extract parameters from the data object + data = call.data[HA_SERVICE_ATTR_DATA] + evt_type = data.get("evt_type", 7) + ctrl = data.get("ctrl") + pars = data.get("pars") _LOGGER.info( - "Testing protocol parameters: evt_type=%d, ctrl=%s, pars_evt_type=%s", + "Testing protocol parameters: evt_type=%d, ctrl=%s, pars=%s", evt_type, ctrl, - pars_evt_type, + pars, ) # Test the specific parameter combination response = await coordinator.test_protocol_parameters( - evt_type, ctrl, pars_evt_type + evt_type, ctrl, pars ) result = { "evt_type": evt_type, "ctrl": ctrl, - "pars_evt_type": pars_evt_type, + "pars": pars, "success": response is not None, "response": response if response else None, } if response: _LOGGER.info( - "✓ Found data: evt_type=%d, ctrl=%s, pars_evt_type=%s", + "Found data: evt_type=%d, ctrl=%s, pars=%s", evt_type, ctrl, - pars_evt_type, + pars, ) _LOGGER.info("Response: %s", json.dumps(response, indent=2)) else: _LOGGER.warning( - "✗ No data: evt_type=%d, ctrl=%s, pars_evt_type=%s", + "No data: evt_type=%d, ctrl=%s, pars=%s", evt_type, ctrl, - pars_evt_type, + pars, ) return result diff --git a/custom_components/blanco_unit/services.yaml b/custom_components/blanco_unit/services.yaml index cdf4a08..2e88b27 100644 --- a/custom_components/blanco_unit/services.yaml +++ b/custom_components/blanco_unit/services.yaml @@ -68,7 +68,7 @@ change_pin: scan_protocol_parameters: name: Scan Protocol Parameters - description: Test a specific combination of protocol parameters (evt_type, ctrl, pars_evt_type). Results are returned and also logged to Home Assistant debug log. This is a diagnostic tool for discovering supported device commands. + description: Test protocol parameters by sending custom BLE commands. Results are returned and logged to debug. Use evt_type (required), ctrl (optional), and pars (optional) to construct the request. fields: device_id: name: Device @@ -78,33 +78,19 @@ scan_protocol_parameters: selector: device: integration: blanco_unit - evt_type: - name: Event Type - description: The evt_type value to test. + data: + name: Data + description: JSON object with evt_type (required), ctrl (optional), and pars (optional) fields. Example to get system info - {"evt_type":7, "ctrl":3, "pars":{"evt_type":2}}. required: true - example: 5 + default: + evt_type: 7 + ctrl: 3 + pars: + evt_type: 2 + example: + evt_type: 7 + ctrl: 3 + pars: + evt_type: 6 selector: - number: - min: 0 - max: 255 - mode: box - ctrl: - name: Control - description: The ctrl value to test (optional, leave empty to test without ctrl). - required: false - example: 1 - selector: - number: - min: 0 - max: 255 - mode: box - pars_evt_type: - name: Parameter Event Type - description: The pars_evt_type value to test (optional, leave empty to test without pars_evt_type). - required: false - example: 2 - selector: - number: - min: 0 - max: 255 - mode: box + object: From 1d02146fda7a87db77eff58e813907da15dada3a Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:37:40 +0100 Subject: [PATCH 5/7] add information to readme --- .coverage | Bin 53248 -> 53248 bytes BLUETOOTH_PROTOCOL.md | 6 +++- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/.coverage b/.coverage index 37557c2dc345a40d29eb2aacb3e18199e0162928..f372c82036d4ae8c6ee0141b1b2efb2c52a8a737 100644 GIT binary patch delta 104 zcmV-u0GI!OpaX!Q1F!~wASwV4`48L=(GRB&H4h{Y01osH-44zU!w#_ypAJzDDzgy~ z1P+raj@A|;2Lu5LMh99K07~a?_=|^fy?XNRDw7tEgAN1$fB^p-007J6^9I}jlc$d~ K0Y9_ck0d|>?<9@@ delta 105 zcmV-v0G9uNpaX!Q1F!~wASnP3`48L=(GRB&H4h{X{|@sG+z!nS!Va(wo(@nBDYFp} z0}hiaj@B0=2Lu5LNC#RM04SZm;V-@o%Ju5WzpIlLkAn^f00060IRF5b$>$BY1CytZ LGy(s!+>azcbhai= diff --git a/BLUETOOTH_PROTOCOL.md b/BLUETOOTH_PROTOCOL.md index 8a29f59..76a99ba 100644 --- a/BLUETOOTH_PROTOCOL.md +++ b/BLUETOOTH_PROTOCOL.md @@ -252,7 +252,7 @@ Example: 4. Split on `0x00` to extract JSON 5. Parse JSON string -## Event Types +## Event Types (Blanco Drink.soda) ### Main Event Types @@ -288,6 +288,10 @@ When setting device configuration, use event type 7 with control code 5 and one | 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. + ## Request/Response Examples ### 1. Initial Pairing diff --git a/README.md b/README.md index 4418d31..08742fe 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,69 @@ data: update_config: true ``` +### blanco_unit.scan_protocol_parameters + +Test protocol parameters by sending custom BLE commands to the device. This is a diagnostic tool for developers and advanced users to discover supported device commands or test protocol behavior. Results are returned in the service response and logged to the debug log. + +**Parameters:** + +- `device_id` (required): The Blanco Unit device to test +- `data` (required): JSON object containing the protocol parameters + - `evt_type` (required): Event type value (0-255) + - `ctrl` (optional): Control code value (0-255) + - `pars` (optional): Parameters dictionary to send with the request + +**Common Protocol Combinations:** + +See [Event Types](BLUETOOTH_PROTOCOL.md#event-types-blanco-drinksoda) in the protocol documentation for complete details. + +- **Get System Information**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 2}}` +- **Get Device Status**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 6}}` +- **Get Device Settings**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 5}}` +- **Get Error Information**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 4}}` + +**Example - Get System Information:** + +```yaml +service: blanco_unit.scan_protocol_parameters +data: + device_id: abc123def456 + data: + evt_type: 7 + ctrl: 3 + pars: + evt_type: 2 +response_variable: result +``` + +**Example - Get Device Status:** + +```yaml +service: blanco_unit.scan_protocol_parameters +data: + device_id: abc123def456 + data: + evt_type: 7 + ctrl: 3 + pars: + evt_type: 6 +response_variable: status +``` + +The service returns a response with the following structure: + +```yaml +evt_type: 7 +ctrl: 3 +pars: + evt_type: 2 +success: true +response: + # Device response data +``` + +For detailed protocol information including all event types, control codes, and parameter formats, see the [Bluetooth Protocol Documentation](BLUETOOTH_PROTOCOL.md). + ## Example Automations ### Low Filter Notification From 17fca6b5a3381646c444ea6bee910eb614cda2e7 Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:38:02 +0100 Subject: [PATCH 6/7] formatting --- BLUETOOTH_PROTOCOL.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/BLUETOOTH_PROTOCOL.md b/BLUETOOTH_PROTOCOL.md index 76a99ba..154622f 100644 --- a/BLUETOOTH_PROTOCOL.md +++ b/BLUETOOTH_PROTOCOL.md @@ -256,37 +256,37 @@ Example: ### Main Event Types -| Event Type | Control Code | Description | Parameters (`pars`) | -| ---------- | ------------ | ------------------------------------- | ------------------------------------------------------------------------------------ | -| 7 | 2 | Get device identity | `{}` - Returns serial number and service code | -| 7 | 3 | Get device information | `{"evt_type": }` - See sub-event types below | -| 7 | 5 | Set device settings | See settings parameters below | -| 7 | 10 | Get WiFi information | `{}` - Returns WiFi SSID, signal, IP, MAC addresses, gateway, subnet | -| 7 | 13 | Change PIN | `{"new_pass": "<5_digit_pin>"}` - Changes device PIN (string) | -| 7 | 1000 | Dispense water | `{"disp_amt": , "co2_int": }` - Amount: 100-1500ml (multiples of 100), Intensity: 1=still, 2=medium, 3=high | -| 10 | N/A | Initial pairing/authentication | `{}` - Returns device ID on successful authentication | +| Event Type | Control Code | Description | Parameters (`pars`) | +| ---------- | ------------ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| 7 | 2 | Get device identity | `{}` - Returns serial number and service code | +| 7 | 3 | Get device information | `{"evt_type": }` - See sub-event types below | +| 7 | 5 | Set device settings | See settings parameters below | +| 7 | 10 | Get WiFi information | `{}` - Returns WiFi SSID, signal, IP, MAC addresses, gateway, subnet | +| 7 | 13 | Change PIN | `{"new_pass": "<5_digit_pin>"}` - Changes device PIN (string) | +| 7 | 1000 | Dispense water | `{"disp_amt": , "co2_int": }` - Amount: 100-1500ml (multiples of 100), Intensity: 1=still, 2=medium, 3=high | +| 10 | N/A | Initial pairing/authentication | `{}` - Returns device ID on successful authentication | ### Sub-Event Types (evt_type in pars for ctrl=3) When using event type 7 with control code 3, you must specify a sub-event type in the `pars` field: -| evt_type | Description | Returns | -| -------- | ------------------------------- | ------------------------------------------------------------------------------------------------------ | -| 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 | +| evt_type | Description | Returns | +| -------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| 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 | ### 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": }, "set_point_heating": {"val": 65}}` | 4-10°C | Set target cooling temperature (heating is always 65°C) | -| 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| +| 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) | +| 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) From 3ca11b0382f22fa486beb5249a5c9a6fbab4f77c Mon Sep 17 00:00:00 2001 From: Nailik <13292441+Nailik@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:40:37 +0100 Subject: [PATCH 7/7] add information to readme --- README.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/README.md b/README.md index 08742fe..b11ebf7 100644 --- a/README.md +++ b/README.md @@ -219,17 +219,10 @@ Test protocol parameters by sending custom BLE commands to the device. This is a - `ctrl` (optional): Control code value (0-255) - `pars` (optional): Parameters dictionary to send with the request -**Common Protocol Combinations:** +**Example - Get System Information for Blanco Drink.soda:** See [Event Types](BLUETOOTH_PROTOCOL.md#event-types-blanco-drinksoda) in the protocol documentation for complete details. -- **Get System Information**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 2}}` -- **Get Device Status**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 6}}` -- **Get Device Settings**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 5}}` -- **Get Error Information**: `{"evt_type": 7, "ctrl": 3, "pars": {"evt_type": 4}}` - -**Example - Get System Information:** - ```yaml service: blanco_unit.scan_protocol_parameters data: @@ -268,8 +261,6 @@ response: # Device response data ``` -For detailed protocol information including all event types, control codes, and parameter formats, see the [Bluetooth Protocol Documentation](BLUETOOTH_PROTOCOL.md). - ## Example Automations ### Low Filter Notification