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>
59 lines
1.8 KiB
Python
59 lines
1.8 KiB
Python
import esphome.codegen as cg
|
|
import esphome.config_validation as cv
|
|
from esphome.components import text_sensor
|
|
|
|
from . import AP2Hub
|
|
|
|
CONF_AP2_HUB_ID = "ap2_hub_id"
|
|
CONF_SLOT = "slot"
|
|
CONF_TYPE = "type"
|
|
|
|
# "status" / "serial" / "firmware" are per-slot; "scan_status" and "devices_found" are hub-level.
|
|
TYPES = ["status", "serial", "firmware", "scan_status", "devices_found"]
|
|
_SLOT_TYPES = ("status", "serial", "firmware")
|
|
|
|
|
|
def _validate(config):
|
|
has_slot = CONF_SLOT in config
|
|
t = config.get(CONF_TYPE)
|
|
if t is None:
|
|
t = "status" if has_slot else None
|
|
if t is None:
|
|
raise cv.Invalid(
|
|
"set 'type: scan_status'/'devices_found', or give a 'slot' for a status/serial sensor"
|
|
)
|
|
config[CONF_TYPE] = t
|
|
if t in _SLOT_TYPES and not has_slot:
|
|
raise cv.Invalid(f"type '{t}' requires a 'slot'")
|
|
if t not in _SLOT_TYPES and has_slot:
|
|
raise cv.Invalid(f"type '{t}' must not have a 'slot'")
|
|
return config
|
|
|
|
|
|
CONFIG_SCHEMA = cv.All(
|
|
text_sensor.text_sensor_schema().extend(
|
|
{
|
|
cv.GenerateID(CONF_AP2_HUB_ID): cv.use_id(AP2Hub),
|
|
cv.Optional(CONF_TYPE): cv.one_of(*TYPES, lower=True),
|
|
cv.Optional(CONF_SLOT): cv.int_range(min=0, max=1),
|
|
}
|
|
),
|
|
_validate,
|
|
)
|
|
|
|
|
|
async def to_code(config):
|
|
var = await text_sensor.new_text_sensor(config)
|
|
parent = await cg.get_variable(config[CONF_AP2_HUB_ID])
|
|
t = config[CONF_TYPE]
|
|
if t == "status":
|
|
cg.add(parent.set_status_sensor(config[CONF_SLOT], var))
|
|
elif t == "serial":
|
|
cg.add(parent.set_serial_sensor(config[CONF_SLOT], var))
|
|
elif t == "firmware":
|
|
cg.add(parent.set_firmware_sensor(config[CONF_SLOT], var))
|
|
elif t == "scan_status":
|
|
cg.add(parent.set_scan_status_sensor(var))
|
|
else:
|
|
cg.add(parent.set_devices_found_sensor(var))
|