# Dyson Air Purifier — Device Specification
# Copyright 2026 Pigs Can Fly Labs LLC
# SPDX-License-Identifier: Apache-2.0

device:
  name: "Dyson Air Purifier"
  manufacturer: "Dyson"
  manufacturer_status: "active"
  protocol: "wifi"
  transport: "mqtt"
  notes: >
    Dyson connected purifiers advertise a local MQTT broker on port 1883 via
    mDNS. The device is the broker. Local credentials are derived from the
    device serial and WiFi password during/after pairing. Known topics are
    scoped by serial, and local control does not require internet access after
    pairing.
  identification:
    mdns_service_type: "_dyson_mqtt._tcp.local."
    default_port: 1883
    identity_keys:
      primary: "serial"
      display: "name"

  discovery:
    methods:
      - type: "mdns"
        mdns:
          service_type: "_dyson_mqtt._tcp.local."
          port: 1883
          identity_mapping:
            stable_keys:
              - source: "hostname"
                key: "serial"
            display:
              source: "name"
    identity:
      stable_keys: ["serial"]
      display: "name"
    static_ip_required: false

  setup:
    required: true
    confidence: "medium"
    notes: >
      Dyson machines are one of the few devices here whose local credentials
      are printed on the hardware rather than issued by a server. That makes
      them fully recoverable without the vendor cloud — but it also means the
      sticker (and the spare label in the box) is the only copy of the secret.
      Photograph it before mounting the machine somewhere awkward.
    methods:
      - type: "softap_http"
        verified: false
        description: >
          The machine hosts its own WPA-protected access point whose SSID and
          passphrase are printed on the device sticker. The app joins that AP
          and hands over the home network credentials.
        softap:
          ssid_prefix: "DYSON-"
          ssid_examples: ["DYSON-XX-XXXXXXXX-475"]
          open_network: false
          passphrase_source: "Printed on the device sticker and on the spare label supplied in the box."
        steps:
          - action: "Read the product SSID and WiFi password from the sticker on the machine."
            actor: "user"
          - action: "Join the machine's access point."
            actor: "user"
          - action: "Send the home network credentials to the machine."
            actor: "client"
            notes: "Exact request layout not captured in this project; the vendor app performs this step today."
          - action: "Rejoin the home network and confirm the machine advertises _dyson_mqtt._tcp.local."
            actor: "client"
            timeout_seconds: 120
    factory_reset:
      confidence: "low"
      effect: "Clears the stored WiFi credentials and returns the machine to hosting its setup AP. The sticker credentials are hardware identity and do not change."
      procedures:
        - name: "Standby button held"
          hold_seconds: 20
          indicator: "The display flashes to confirm the WiFi reset."
          steps:
            - action: "With the machine powered on, press and hold the standby button on the unit (not the remote) until the display flashes."
              actor: "user"
              notes: "Hold time varies by model; verify against the model's manual."
    rejoin:
      in_place_supported: false
      requires_factory_reset: true
      notes: >
        Re-provisioning goes back through the setup AP, so a router change
        means resetting the machine's WiFi and repeating onboarding. The local
        MQTT credentials are unaffected because they are derived from the
        sticker, not from the network.
    credentials:
      wifi_passphrase_protection: "unknown"
      stored_on_device:
        - "Home network credentials"
      issued_to_client:
        - "Local MQTT username — the device serial number"
        - "Local MQTT password — base64 of the SHA-512 digest of the sticker WiFi password"
      notes: >
        The MQTT password derivation is the published libdyson/ha-dyson
        behaviour: hash the sticker password with SHA-512 and base64-encode the
        raw digest. Because it is derived from a printed static secret, anyone
        who photographs the sticker keeps local control forever — including
        after a resale.

  variants:
    - model: "F3H"
      name: "Dyson Pure Cool"
      identification:
        hostname_pattern: "F3H-*.local."
      characteristics:
        - "Air purifier and fan with local MQTT broker."
    - model: "PE6"
      name: "Dyson Humidify+Cool"
      identification:
        hostname_pattern: "PE6-*.local."
      characteristics:
        - "Air purifier, humidifier, and fan with local MQTT broker."

evidence:
  live_lan_probe:
    date: "2026-07-16"
    devices:
      - model: "F3H Pure Cool"
        serial: "F3H-US-PFA5664A"
        address: "10.69.194.164"
        mdns_service_type: "_dyson_mqtt._tcp"
        port: 1883
      - model: "PE6 Humidify+Cool"
        serial: "PE6-US-KDA2129A"
        address: "10.69.194.165"
        mdns_service_type: "_dyson_mqtt._tcp"
        port: 1883

mqtt_topics:
  - topic: "{serial}/status/current"
    name: "Current Status"
    direction: "subscribe"
    description: "State reports including fan mode, oscillation, air quality, filter state, temperature, humidity, and device mode."
    qos: 0
    payload_format:
      encoding: "json"
  - topic: "{serial}/status/connection"
    name: "Connection Status"
    direction: "subscribe"
    description: "Local MQTT connection and device availability status."
    qos: 0
    payload_format:
      encoding: "json"
  - topic: "{serial}/command"
    name: "Command"
    direction: "publish"
    description: "Command topic for mode, power, fan speed, oscillation, auto mode, and humidifier controls."
    qos: 0
    payload_format:
      encoding: "json"

entities:
  - platform: "fan"
    name: "Purifier Fan"
    state_topic: "{serial}/status/current"
    commands:
      turn_on: "{serial}/command"
      turn_off: "{serial}/command"
      set_percentage: "{serial}/command"
      set_oscillating: "{serial}/command"
  - platform: "sensor"
    name: "Air Quality"
    state_topic: "{serial}/status/current"
  - platform: "sensor"
    name: "Filter Life"
    state_topic: "{serial}/status/current"

