HomeSpan Documentation

repository·master·Indexed 24 days ago

https://github.com/homespan/homespan

An Arduino library for ESP32 (S2, S3, C3, C5, C6) that enables the creation of HAP-R2 compliant HomeKit accessories. It provides a service-centric framework supporting Accessory and Bridge modes, WiFi/Ethernet connectivity, and OTA updates. Key features include a Command-Line Interface (CLI) for diagnostics and configuration, device cloning for hardware replacement, and a Blinkable interface for custom status indicators.

Tokens
53.2K
Snippets
29
Records
301
Agent score
79%

What's inside HomeSpan

  1. What is HomeSpan Pairing Data?

    master

    HomeSpan Pairing Data is a collection of unique identifiers required for a device to be recognized by HomeKit and maintain existing automations/scenes. It consists of:

    • Accessory Data: The 17-character Device ID, 32-byte long-term public key (LTPK), and 64-byte long-term secret key (LTSK).
    • Controller Data: The 36-character Device ID and 32-byte LTPK for each paired HomeKit Controller (e.g., Apple TV, HomePod).

    This data is stored in the ESP32's non-volatile storage (NVS). If you replace a device with a new one that has different Pairing Data, HomeKit will treat it as a brand-new accessory, and all existing automations and scenes associated with the old device will be lost.

  2. What is SpanPoint and how to use it for ESP32 communication

    master

    SpanPoint is an implementation of the Espressif ESP-NOW protocol for HomeSpan. It enables bi-directional, point-to-point communication of small, fixed-size messages directly between ESP32 devices using their MAC Addresses, without requiring a central WiFi network.

    To establish connectivity, you must instantiate a SpanPoint object on each device referencing the other device's MAC Address. For two devices to be considered "complementary" (able to communicate), their sendSize and receiveSize parameters must match cross-wise (e.g., Device A's sendSize must equal Device B's receiveSize), though setting either to 0 is permitted if that direction of communication is not needed.

  3. Use SpanPoint for battery-powered remote sensors

    master

    SpanPoint enables point-to-point communication between ESP32 devices using ESP-NOW, which does not require an always-on WiFi connection to a central network. This allows for a hybrid architecture:

    1. Main Device (Wall-powered): Runs a full HomeSpan sketch and maintains a constant WiFi connection to HomeKit. It acts as a gateway, receiving data from remote devices via SpanPoint.
    2. Remote Device (Battery-powered): A lightweight device that takes periodic measurements and transmits them via SpanPoint to the Main Device. These devices can use power-management techniques like deep-sleep to extend battery life.

    For a complete implementation, refer to the Arduino IDE examples: File → Examples → HomeSpan → Other Examples → RemoteSensors.

  4. How to create a custom StepperControl driver

    master

    If your specific motor driver chip is not supported by existing classes, you can create a custom driver by inheriting from the StepperControl abstract class. The core logic for background movement is already handled by the base class; you only need to implement the hardware-specific pin toggling.

    Required Method:

    • void onStep(boolean direction): Logic to advance the motor by a single step based on the direction parameter.

    Optional Methods (implement if applicable to your hardware):

    • void onEnable(): Logic to enable the motor driver.
    • void onDisable(): Logic to disable the motor driver.
    • void onBrake(): Logic to put the motor into a short brake state.
    • StepperControl *setStepType(int mode): Logic to set the step type mode (e.g., Full Step, Half Step) based on the mode parameter.

    Implementation Example (Stepper_A3967):

    struct Stepper_A3967 : StepperControl {
      // ... constructor and pin definitions ...
    
      void onStep(boolean direction) override {
        digitalWrite(dirPin,direction);
        digitalWrite(stepPin,HIGH);
        digitalWrite(stepPin,LOW);      
      }
    
      void onEnable() override {
        digitalWrite(enablePin,0);
      }
    
      void onDisable() override {
        digitalWrite(enablePin,1);
      }
    
      StepperControl *setStepType(int mode) override {
        switch(mode){
          case FULL_STEP_TWO_PHASE:
            digitalWrite(m1Pin,LOW);
            digitalWrite(m2Pin,LOW);
            break;
          // ... other cases ...
        }
        return(this);
      }
    };
    #include "HomeSpan.h"
    
    //////////////////////////
     
    struct Stepper_A3967 : StepperControl {
    
      int m1Pin;
      int m2Pin;
      int stepPin;
      int dirPin;
      int enablePin;
    
    //////////////////////////
    
      Stepper_A3967(int m1Pin, int m2Pin, int stepPin, int dirPin, int enablePin, std::pair<uint32_t, uint32_t> taskParams = {1,0}) : StepperControl(taskParams.first,taskParams.second){
        this->m1Pin=m1Pin;
        this->m2Pin=m2Pin;
        this->stepPin=stepPin;
        this->dirPin=dirPin;
        this->enablePin=enablePin;
    
        pinMode(m1Pin,OUTPUT);
        pinMode(m2Pin,OUTPUT);
        pinMode(stepPin,OUTPUT);
        pinMode(dirPin,OUTPUT);
        pinMode(enablePin,OUTPUT);
    
        setStepType(FULL_STEP_TWO_PHASE);
      }
    
    //////////////////////////
    
      void onStep(boolean direction) override {
        digitalWrite(dirPin,direction);
        digitalWrite(stepPin,HIGH);
        digitalWrite(stepPin,LOW);      
      }
    
    //////////////////////////
    
      void onEnable() override {
        digitalWrite(enablePin,0);
      }
    
    //////////////////////////
    
      void onDisable() override {
        digitalWrite(enablePin,1);
      }
    
    //////////////////////////
    
      StepperControl *setStepType(int mode) override {
        switch(mode){
          case FULL_STEP_TWO_PHASE:
            digitalWrite(m1Pin,LOW);
            digitalWrite(m2Pin,LOW);
            break;
          case HALF_STEP:
            digitalWrite(m1Pin,HIGH);
            digitalWrite(m2Pin,LOW);
            break;
          case QUARTER_STEP:
            digitalWrite(m1Pin,LOW);
            digitalWrite(m2Pin,HIGH);
            break;
          case EIGHTH_STEP:
            digitalWrite(m1Pin,HIGH);
            digitalWrite(m2Pin,HIGH);
            break;
          default:
            ESP_LOGE(STEPPER_TAG,"Unknown StepType=%d",mode);
        }
        return(this);
      }
      
    };
  5. Use ServiceLabel to name un-nameable services

    master

    The ServiceLabel (CC) service provides a naming scheme for services that cannot be named directly (like a StatelessProgrammableSwitch).

    To use it:

    1. Link the un-nameable services to a ServiceLabel service.
    2. Each linked service must include a ServiceLabelIndex characteristic with a unique value.
    3. Configure ServiceLabelNamespace (CD) to determine how they appear in the Home App (0: DOTS, 1: NUMERALS).
  6. Use the RFControl class for RF/IR signal generation

    master

    The RFControl class interfaces with the ESP32 RMT peripheral to drive RF or IR transmitters. You can use it to control appliances directly from the Home App or Siri.

    There are two primary ways to use RFControl:

    1. Internal Memory Method: Use clear(), add(), or phase() to build a pulse train in the object's internal memory, then call start() to transmit it. This is best for pulse trains created on-the-fly.
    2. External Array Method: Pre-compute pulse trains in external 32-bit arrays using the RF_PULSE(highTicks, lowTicks) macro, then call start() with a pointer to that array. This is best for pre-defined, static signals and must use RAM (not PSRAM).
    RFControl rf(11);  // create an instance of RFControl
    
    // Method 1: Internal Memory
    rf.clear();        // clear the internal memory structure
    rf.add(100,50);    // create pulse of 100 ticks HIGH followed by 50 ticks LOW
    rf.start(4,1000);  // start transmission; repeat 4 cycles; 1 tick = 1000µs 
    
    // Method 2: External Array
    uint32_t pulseTrain[] = {RF_PULSE(100,50), RF_PULSE(100,50), RF_PULSE(25,500)};
    rf.start(pulseTrain,3,4,1000);  // start transmission using the same parameters
  7. How the HAP Accessory Attribute Database works

    master

    HomeSpan uses a declarative approach to define the HAP (HomeKit Accessory Protocol) Accessory Attribute Database. Instead of managing complex object trees, you instantiate objects in a specific order, and HomeSpan automatically handles the registration and hierarchy.

    The Hierarchy Rules

    1. SpanAccessory: The top-level container. Instantiating new SpanAccessory() creates a new accessory and registers it with the global homeSpan object.
    2. Service: Services are defined in the Service:: namespace. Instantiating a service (e.g., new Service::LightBulb()) automatically attaches it to the last instantiated SpanAccessory.
    3. Characteristic: Characteristics are defined in the Characteristic:: namespace. Instantiating a characteristic (e.g., new Characteristic::On()) automatically attaches it to the last instantiated Service.

    Critical Note: The order of instantiation is vital. If you instantiate objects in the wrong order, the hierarchy will be incorrect. HomeSpan performs validation at startup; if an accessory is missing required services or characteristics (like Service::AccessoryInformation or Characteristic::Identify), HomeSpan will report errors and halt the program.

  8. How HomeSpan handles network connectivity callbacks

    master

    HomeSpan considers network connectivity to be established as soon as the device receives its first IP address (IPv4 or IPv6).

    • The callback function registered via homeSpan.setConnectionCallback() is triggered only once upon reception of that first address.
    • HomeSpan will not trigger this callback again if additional IP addresses (such as multiple IPv6 addresses) are acquired.
    • For every subsequent IP address acquired, HomeSpan creates a Web Log entry and a Serial Monitor report, but does not execute the user-defined callback.

    If you require a callback to trigger for every individual IP address acquired (e.g., to track Link Local, Unique Local, and Global addresses separately), you should use the native Arduino-ESP32 network event handlers: WiFi.onEvent(), ETH.onEvent(), or Network.onEvent().

  9. Use HomeSpan Device Cloning to replace hardware

    master
    HomeSpan provides a cloning feature that allows you to seamlessly swap a broken device for a new one. This process ensures you do not need to re-pair the device or lose existing HomeKit automations.
  10. Access Characteristics from outside their Services

    master

    You can access the Characteristics of a Service from outside the scope of that Service (for example, from within the main Arduino loop()).

    In the ExternalReference example, this technique is used to check the state of two different LEDs and automatically turn them off if both are detected as being ON simultaneously.

  11. Understand Stepper Motor Modes in StepperControl

    master

    The StepperControl class supports several stepping patterns (modes) that determine the step size, smoothness, and torque consistency of a stepper motor. The choice of mode depends on your hardware capabilities (specifically PWM support) and your application's requirements for precision and torque.

    Core Concepts

    • Step Size: The angular distance the motor moves in one step. Smaller steps (e.g., Quarter Step) provide higher granularity but require more steps for a full rotation.
    • Smoothness: How fluidly the motor rotates. PWM-based modes are generally smoother than non-PWM modes.
    • Torque Consistency: The amount of holding/rotational force. Non-PWM modes (like Half Step) can suffer from uneven torque, whereas PWM modes use sinusoidal current patterns to maintain constant power ($cos^2 + sin^2 = 1$).
  12. Implement periodic updates and Event Notifications with loop()

    master

    To simulate sensors (like Temperature or Air Quality) or handle periodic tasks, implement the virtual loop() method in your derived Service. This method is called repeatedly.

    To notify HomeKit of a change (an Event Notification):

    1. Use setVal() to update the Characteristic value.
    2. Use timeVal() to keep track of elapsed time since the last update if needed.