# Example ESPHome config: TWO xTool SafetyPro AP2 purifiers on ONE ESP32, # each exposed to Home Assistant as a fan. MACs are set at RUNTIME from HA # (no reflash) and persisted to flash — leave them blank, flash once, then # either type each AP2's MAC into the "Purifier N MAC" text entity, or press # "Purifier Scan" to discover powered AP2s on-device and auto-fill any free slot. esphome: name: ap2-hub esp32: board: esp32dev framework: type: esp-idf logger: # version 2 + local embeds the web UI in flash (no internet CDN) — required on # isolated networks where the device can't reach oi.esphome.io. web_server: version: 2 local: true api: ota: - platform: esphome wifi: ssid: !secret wifi_ssid password: !secret wifi_password external_components: - source: type: local path: components components: [ap2_hub] # Or from GitHub: # - source: github://YOURUSER/esphome-xtool-ap2@main # components: [ap2_hub] # BLE security: bond with the AP2 (Just Works, no MITM) so it remembers this ESP32 esp32_ble: io_capability: none auth_req_mode: bond # The hub needs the BLE tracker scanning so slots can auto-connect to their MAC. esp32_ble_tracker: ap2_hub: id: ap2 # --- Per-slot MAC inputs (settable from HA; empty clears the slot) --- text: - platform: ap2_hub ap2_hub_id: ap2 slot: 0 name: "Purifier 1 MAC" mode: text - platform: ap2_hub ap2_hub_id: ap2 slot: 1 name: "Purifier 2 MAC" mode: text # --- Per-slot fan: on/off + speeds 1..4 (gear A0..A4) --- fan: - platform: ap2_hub ap2_hub_id: ap2 slot: 0 name: "Purifier 1" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 name: "Purifier 2" # --- Per-slot connection state + a hub-level "scan running" flag --- binary_sensor: - platform: ap2_hub ap2_hub_id: ap2 slot: 0 name: "Purifier 1 BLE Connected" device_class: connectivity - platform: ap2_hub ap2_hub_id: ap2 slot: 1 name: "Purifier 2 BLE Connected" device_class: connectivity # Bonded = a BLE bond is stored for this slot's MAC (true even while powered off). # This is what makes auto-reconnect-on-power-up work; a MAC alone is not enough. - platform: ap2_hub ap2_hub_id: ap2 type: bonded slot: 0 name: "Purifier 1 BLE Bonded" - platform: ap2_hub ap2_hub_id: ap2 type: bonded slot: 1 name: "Purifier 2 BLE Bonded" - platform: ap2_hub ap2_hub_id: ap2 type: scan_active name: "Purifier Scan Active" device_class: running # --- Per-slot link status + serial + firmware; hub-level scan status / devices --- text_sensor: - platform: ap2_hub ap2_hub_id: ap2 slot: 0 name: "Purifier 1 BLE Status" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 name: "Purifier 2 BLE Status" - platform: ap2_hub ap2_hub_id: ap2 type: serial slot: 0 name: "Purifier 1 Serial" - platform: ap2_hub ap2_hub_id: ap2 type: serial slot: 1 name: "Purifier 2 Serial" - platform: ap2_hub ap2_hub_id: ap2 type: firmware slot: 0 name: "Purifier 1 Firmware" - platform: ap2_hub ap2_hub_id: ap2 type: firmware slot: 1 name: "Purifier 2 Firmware" - platform: ap2_hub ap2_hub_id: ap2 type: scan_status name: "Purifier Scan Status" - platform: ap2_hub ap2_hub_id: ap2 type: devices_found name: "Purifier Devices Found" # --- Per-slot filter life % (from M9033: H I J K L S). Omit any you don't have. --- sensor: - platform: ap2_hub ap2_hub_id: ap2 slot: 0 type: pre_filter name: "Purifier 1 Filter Pre" - platform: ap2_hub ap2_hub_id: ap2 slot: 0 type: medium_filter name: "Purifier 1 Filter Medium" - platform: ap2_hub ap2_hub_id: ap2 slot: 0 type: activated_carbon name: "Purifier 1 Filter Activated Carbon" - platform: ap2_hub ap2_hub_id: ap2 slot: 0 type: carbon_cloth name: "Purifier 1 Filter Carbon Cloth" - platform: ap2_hub ap2_hub_id: ap2 slot: 0 type: formaldehyde name: "Purifier 1 Filter Formaldehyde" - platform: ap2_hub ap2_hub_id: ap2 slot: 0 type: hepa name: "Purifier 1 Filter HEPA" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 type: pre_filter name: "Purifier 2 Filter Pre" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 type: medium_filter name: "Purifier 2 Filter Medium" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 type: activated_carbon name: "Purifier 2 Filter Activated Carbon" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 type: carbon_cloth name: "Purifier 2 Filter Carbon Cloth" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 type: formaldehyde name: "Purifier 2 Filter Formaldehyde" - platform: ap2_hub ap2_hub_id: ap2 slot: 1 type: hepa name: "Purifier 2 Filter HEPA" # --- Scan trigger + duration (built-in template platforms drive the hub) --- button: # Start a scan, or cancel one that's already running (start/stop toggle). - platform: template name: "Purifier Scan" icon: mdi:radar on_press: - lambda: "id(ap2).toggle_scan();" # Force an immediate (active) reconnect attempt on all slots. - platform: template name: "Purifier Reconnect" icon: mdi:bluetooth-connect on_press: - lambda: "id(ap2).reconnect_all();" # Wipe stored BLE bonds — forces a fresh pair on the next pairing-mode connect. - platform: template name: "Purifier Clear Bonds" icon: mdi:bluetooth-off entity_category: config on_press: - lambda: "id(ap2).clear_bonds();" # Toggle each AP2's buzzer (M9046). State shown by the "… Buzzer" binary_sensor. - platform: template name: "Purifier 1 Buzzer Toggle" icon: mdi:volume-high on_press: - lambda: "id(ap2).buzzer_toggle(0);" - platform: template name: "Purifier 2 Buzzer Toggle" icon: mdi:volume-high on_press: - lambda: "id(ap2).buzzer_toggle(1);" number: - platform: template name: "Purifier Scan Duration" id: ap2_scan_duration optimistic: true restore_value: true initial_value: 90 min_value: 30 max_value: 180 step: 10 unit_of_measurement: s mode: box # Applies on user change AND on restore at boot. on_value: - lambda: "id(ap2).set_scan_duration((uint16_t) x);"