Files
esphome-xtool-ap2/ap2-hub.example.yaml
T
mrambossekandClaude Opus 4.8 878f7c9c27 Initial commit: esphome-xtool-ap2 ESPHome component
Control one or two xTool SafetyPro AP2 air purifiers over BLE and expose them to Home Assistant via an ESP32 (the ap2_hub external component): per-slot fan control with live gear sync, runtime-settable flash-persisted MACs, an on-device scanner, M9033 status polling (filter life, serial, firmware), buzzer control, and BLE bonding. Includes the PC debug tool (tools/ap2_ble.py) and the reverse-engineered protocol spec (SPEC.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:45:11 +02:00

253 lines
6.3 KiB
YAML

# 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);"