commit 98f83d58cc11dc8d209fd351febf4f2b4e7a4bd4 parent 34923570a03dcbdfece4400264e4dad4068c3689 Author: Stéphan Kochen <git@stephank.nl> Date: Thu, 03 Sep 2026 22:14:53 +0200 Hide unavailable Smart Hub 75 output
diff --git a/custom_components/inlite/lib/inlite_ble/hub.py b/custom_components/inlite/lib/inlite_ble/hub.py index e84d4949a7c7f7a0b5f514d794af1351ea874605..e2544b7a461b5246e91b8572b4ad2350b857e4f5 100644 --- a/custom_components/inlite/lib/inlite_ble/hub.py +++ b/custom_components/inlite/lib/inlite_ble/hub.py @@ -48,6 +48,7 @@ ACK_TIMEOUT = 2.0 # seconds to wait for hub ACK STREAM_TIMEOUT = 3.0 # seconds to wait for STREAM response BLE_PACKET_PART_SIZE = 78 # Matches the official app's GATT packet splitter. STREAM_DATA_SIZE = 62 # Maximum official-app payload per PKT_BLOCK_DATA. +SMART_HUB_75_PRODUCT_ID = 0x1D class ZoneState: @@ -129,6 +130,8 @@ self._stream_invalid = False self._incoming_ble_buffer = bytearray() self._zone_states: dict[int, ZoneState] = {} self._firmware_version: int | None = None + self._product_id: int | None = None + self._hidden_output_ids: set[int] = set() self._discovered_device_ids: set[int] = set() self._discovery_event = asyncio.Event() self._notification_callback: Callable[[dict[str, Any]], None] | None = None @@ -155,6 +158,10 @@ @property def firmware_version(self) -> int | None: """Firmware byte reported by GET_INFO_DEVICES.""" return self._firmware_version + + def is_output_visible(self, output_id: int) -> bool: + """Return whether an output should be exposed by the integration.""" + return output_id not in self._hidden_output_ids async def scan(self, timeout: float = 10.0) -> BLEDevice | None: """Scan for the in-lite hub by name or address.""" @@ -329,6 +336,9 @@ outlet_id = data[i] output_mode = data[i + 1] output_state = data[i + 2] # data[i + 3] = rtcTimer + if outlet_id in self._hidden_output_ids: + i += 4 + continue if outlet_id in self._zone_states: old = self._zone_states[outlet_id] if old.output_mode != output_mode or old.output_state != output_state: @@ -590,7 +600,7 @@ _LOGGER.warning("Unexpected opcode in STREAM: 0x%04X", opcode) return self._zone_states # vendor_id = stream[5] - # product_id = stream[6] + self._product_id = stream[6] self._firmware_version = stream[7] # status = stream[8] num_zones = stream[9] @@ -599,7 +609,7 @@ if len(stream) != expected_length: _LOGGER.warning("Malformed GET_INFO_DEVICES response: %d bytes, expected %d", len(stream), expected_length) return self._zone_states - zones: dict[int, ZoneState] = {} + zone_records: list[ZoneState] = [] zone_start = 10 for i in range(num_zones): pos = zone_start + i * 7 @@ -612,8 +622,17 @@ dtd1=stream[pos + 4], dtd2=stream[pos + 5], output_state=stream[pos + 6], ) - zones[zs.output_id] = zs + zone_records.append(zs) _LOGGER.debug("Zone %d: %s", zs.output_id, zs) + + self._hidden_output_ids.clear() + if self._product_id == SMART_HUB_75_PRODUCT_ID and len(zone_records) > 2: + # The official app suppresses the final raw zone on a Smart Hub 75. + hidden_zone = zone_records.pop() + self._hidden_output_ids.add(hidden_zone.output_id) + _LOGGER.debug("Ignoring unavailable Smart Hub 75 output %d", hidden_zone.output_id) + + zones = {zone.output_id: zone for zone in zone_records} self._zone_states = zones return zones diff --git a/custom_components/inlite/light.py b/custom_components/inlite/light.py index 7011f605a18c85e6f89a79b964b91231cbe77e75..7b1f62ecfd325ad523cf0c4c66b6789b7d8d910e 100644 --- a/custom_components/inlite/light.py +++ b/custom_components/inlite/light.py @@ -34,10 +34,13 @@ entities: list[InliteLightEntity] = [] for tx_data in entry.data[CONF_TRANSFORMERS]: device_id = tx_data["device_id"] + hub = coordinator.hubs[device_id] tx_name = tx_data["name"] firmware = tx_data.get("firmware_version", "unknown") for zone_data in tx_data["zones"]: + if not hub.is_output_visible(zone_data["output_id"]): + continue entities.append( InliteLightEntity( coordinator=coordinator, diff --git a/tests/test_hub.py b/tests/test_hub.py index 6288831eded0b145957db5cf521cac0cb58ba268..e226dfc2541c0f8ea882f3919173a3f07173f9b6 100644 --- a/tests/test_hub.py +++ b/tests/test_hub.py @@ -55,6 +55,26 @@ def test_zone_states_empty_initially(self) -> None: hub = InliteHub(device_id=1, passphrase="test") assert hub.zone_states == {} + def test_smart_hub_75_hides_third_raw_zone(self) -> None: + async def run() -> InliteHub: + hub = InliteHub(device_id=1, passphrase="test") + + async def get_info(_opcode: int, _data: bytes) -> bytes: + return bytes.fromhex( + "00000205000c1d380003" + "00010102000000" + "01010102000000" + "02010102000000" + ) + + hub._send_acknowledged_command = get_info # type: ignore[method-assign] + await hub.query_zone_states() + return hub + + hub = asyncio.run(run()) + assert set(hub.zone_states) == {0, 1} + assert hub.is_output_visible(2) is False + def test_loop_stored_on_connect(self) -> None: """Verify _loop is set during connect (needed for thread-safe callbacks).""" hub = InliteHub(device_id=1, passphrase="test")