César E. Benavides | Design Engineer & Software Architect
How to Control IR + SubGHz Devices with Homebridge & Flipper Zero

How to Control IR + SubGHz Devices with Homebridge & Flipper Zero

7 min read
Tutorial
Homebridge
Python
TypeScript

How to Control IR + SubGHz Devices with Homebridge & Flipper Zero

Learn how to control IR and Sub-GHz devices using Homebridge and Flipper Zero.

Introduction

My obsessive compulsion to want to control things with my computer and my voice led me down this rabbit hole.

I have been really into home automation lately. I run Homebridge on my Mac Studio, which lets me expose non-HomeKit devices to Apple Home. I also have a Flipper Zero -- a multi-tool device for pentesters and hardware enthusiasts that can interact with IR, SubGHz, NFC, RFID, and more.

The question was simple: could I combine these two things to control dumb devices (like my office fan and ceiling fan) through Siri and Apple Home?

Spoiler: yes, but it took some creative problem-solving.

The Problem

My office has two fans:

  • An office desk fan that uses an IR remote (infrared, line-of-sight)
  • A ceiling fan that uses a SubGHz remote (radio frequency, 433MHz)

Neither of these is a "smart" device. They have no Wi-Fi, no Bluetooth, no HomeKit support. Just plain old remotes.

To make this work, I needed to:

  1. Figure out the frequencies and protocols used by each remote
  2. Install custom firmware on the Flipper Zero (I used Unleashed Firmware) to unlock extended SubGHz frequency support
  3. Capture and decode the remote signals -- I discovered my office fan uses IR commands and my ceiling fan uses SubGHz commands
  4. Find a way to send those signals programmatically from my Mac, through the Flipper, on demand

The Strategy

The plan: build a Homebridge platform plugin that exposes virtual switches to HomeKit. When a switch is toggled, it triggers a command that tells the Flipper Zero to transmit the appropriate IR or SubGHz signal.

Configuring Homebridge

I started with the Homebridge Platform Plugin Template, which gives you a vanilla TypeScript project with the correct structure for a Homebridge plugin.

The core of the plugin is platform.ts, which registers accessories and handles switch events:

platform.ts:

import {
  API,
  DynamicPlatformPlugin,
  Logger,
  PlatformAccessory,
  PlatformConfig,
  Service,
  Characteristic,
} from "homebridge";
import { exec } from "child_process";
import path from "path";
 
const PLUGIN_NAME = "homebridge-flipper-control";
const PLATFORM_NAME = "FlipperControl";
 
interface DeviceConfig {
  name: string;
  type: "ir" | "subghz";
  command: string;
  stateful: boolean;
}
 
export class FlipperControlPlatform implements DynamicPlatformPlugin {
  public readonly Service: typeof Service = this.api.hap.Service;
  public readonly Characteristic: typeof Characteristic =
    this.api.hap.Characteristic;
 
  public readonly accessories: PlatformAccessory[] = [];
 
  constructor(
    public readonly log: Logger,
    public readonly config: PlatformConfig,
    public readonly api: API
  ) {
    this.log.debug("Finished initializing platform:", this.config.name);
 
    this.api.on("didFinishLaunching", () => {
      this.log.debug("Executed didFinishLaunching callback");
      this.discoverDevices();
    });
  }
 
  configureAccessory(accessory: PlatformAccessory) {
    this.log.info("Loading accessory from cache:", accessory.displayName);
    this.accessories.push(accessory);
  }
 
  discoverDevices() {
    const devices: DeviceConfig[] = this.config.devices || [];
 
    for (const device of devices) {
      const uuid = this.api.hap.uuid.generate(device.name);
      const existingAccessory = this.accessories.find(
        (acc) => acc.UUID === uuid
      );
 
      if (existingAccessory) {
        this.log.info(
          "Restoring existing accessory from cache:",
          existingAccessory.displayName
        );
        this.setupAccessory(existingAccessory, device);
      } else {
        this.log.info("Adding new accessory:", device.name);
        const accessory = new this.api.platformAccessory(device.name, uuid);
        this.setupAccessory(accessory, device);
        this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
          accessory,
        ]);
      }
    }
  }
 
  setupAccessory(accessory: PlatformAccessory, device: DeviceConfig) {
    const service =
      accessory.getService(this.Service.Switch) ||
      accessory.addService(this.Service.Switch);
 
    service.setCharacteristic(this.Characteristic.Name, device.name);
 
    let currentState = false;
 
    service
      .getCharacteristic(this.Characteristic.On)
      .onGet(() => currentState)
      .onSet((value) => {
        const isOn = value as boolean;
        currentState = isOn;
 
        this.log.info(`${device.name} -> ${isOn ? "ON" : "OFF"}`);
 
        const scriptPath = path.resolve(__dirname, "../scripts/script.py");
        const command = `python3 ${scriptPath} ${device.type} ${device.command} ${isOn ? "on" : "off"}`;
 
        exec(command, (error, stdout, stderr) => {
          if (error) {
            this.log.error(`Error executing command: ${error.message}`);
            return;
          }
          if (stderr) {
            this.log.warn(`stderr: ${stderr}`);
          }
          this.log.debug(`stdout: ${stdout}`);
        });
 
        if (!device.stateful) {
          setTimeout(() => {
            currentState = false;
            service.updateCharacteristic(this.Characteristic.On, false);
          }, 1000);
        }
      });
  }
}

The Solution: Use Python

Here is where things got interesting. I could not find a reliable way to communicate with the Flipper Zero directly from JavaScript/TypeScript. The serial protocol is finicky, and the existing Node.js libraries were either unmaintained or incomplete.

Then I discovered PyFlipper -- a Python library that provides a clean interface for controlling Flipper Zero over serial. It supports IR transmission, SubGHz transmission, and much more.

So the architecture became: Homebridge (TypeScript) -> Python script (PyFlipper) -> Flipper Zero (USB serial) -> IR/SubGHz signal -> Device.

script.py:

#!/usr/bin/env python3
"""
Flipper Zero IR/SubGHz Control Script
Controls devices via Flipper Zero using PyFlipper.
Used as a bridge between Homebridge and Flipper Zero.
"""
 
import sys
import time
import serial
import serial.tools.list_ports
from pyflipper import PyFlipper
 
 
def find_flipper_port():
    """Auto-detect the Flipper Zero serial port."""
    ports = serial.tools.list_ports.comports()
    for port in ports:
        if "flipper" in port.description.lower() or "flip" in port.device.lower():
            return port.device
    # Fallback to common macOS port pattern
    for port in ports:
        if "usbmodem" in port.device.lower():
            return port.device
    return None
 
 
def send_ir_command(flipper, command, state):
    """Send an IR command via Flipper Zero."""
    print(f"Sending IR command: {command} ({state})")
 
    if state == "on":
        # Navigate to the IR file on Flipper's SD card
        ir_file = f"/ext/infrared/assets/{command}.ir"
        print(f"Transmitting IR file: {ir_file}")
 
        # Use the Flipper's IR subsystem to transmit
        flipper.ir.tx_from_file(ir_file, command)
        time.sleep(0.5)
    elif state == "off":
        ir_file = f"/ext/infrared/assets/{command}.ir"
        print(f"Transmitting IR file: {ir_file}")
        flipper.ir.tx_from_file(ir_file, command)
        time.sleep(0.5)
 
    print("IR command sent successfully")
 
 
def send_subghz_command(flipper, command, state):
    """Send a SubGHz command via Flipper Zero."""
    print(f"Sending SubGHz command: {command} ({state})")
 
    if state == "on":
        subghz_file = f"/ext/subghz/assets/{command}_on.sub"
    else:
        subghz_file = f"/ext/subghz/assets/{command}_off.sub"
 
    print(f"Transmitting SubGHz file: {subghz_file}")
 
    # Read and transmit the SubGHz file
    flipper.subghz.tx_from_file(subghz_file)
    time.sleep(1)
    flipper.subghz.tx_stop()
 
    print("SubGHz command sent successfully")
 
 
def main():
    if len(sys.argv) < 4:
        print("Usage: script.py <type> <command> <state>")
        print("  type: ir | subghz")
        print("  command: name of the signal file")
        print("  state: on | off")
        sys.exit(1)
 
    signal_type = sys.argv[1]
    command = sys.argv[2]
    state = sys.argv[3]
 
    print(f"Signal Type: {signal_type}")
    print(f"Command: {command}")
    print(f"State: {state}")
 
    # Find and connect to Flipper Zero
    port = find_flipper_port()
    if not port:
        print("ERROR: Flipper Zero not found. Is it connected via USB?")
        sys.exit(1)
 
    print(f"Found Flipper Zero on port: {port}")
 
    try:
        flipper = PyFlipper(com=port)
        print("Connected to Flipper Zero")
 
        if signal_type == "ir":
            send_ir_command(flipper, command, state)
        elif signal_type == "subghz":
            send_subghz_command(flipper, command, state)
        else:
            print(f"Unknown signal type: {signal_type}")
            sys.exit(1)
 
    except serial.SerialException as e:
        print(f"Serial connection error: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)
 
    print("Done!")
 
 
if __name__ == "__main__":
    main()

The Result

After getting the Python script working and the Homebridge plugin configured, I successfully ran the code and was able to control both fans from Apple Home and Siri.

The final piece was the Homebridge configuration that ties it all together:

homebridge-config.json:

{
  "platform": "FlipperControl",
  "name": "Flipper Control",
  "devices": [
    {
      "name": "Office Fan",
      "type": "ir",
      "command": "office_fan_power",
      "stateful": true
    },
    {
      "name": "Office Fan Speed",
      "type": "ir",
      "command": "office_fan_speed",
      "stateful": false
    },
    {
      "name": "Office Fan Oscillate",
      "type": "ir",
      "command": "office_fan_oscillate",
      "stateful": true
    },
    {
      "name": "Ceiling Fan",
      "type": "subghz",
      "command": "ceiling_fan",
      "stateful": true
    },
    {
      "name": "Ceiling Fan Light",
      "type": "subghz",
      "command": "ceiling_fan_light",
      "stateful": true
    },
    {
      "name": "Ceiling Fan Speed",
      "type": "subghz",
      "command": "ceiling_fan_speed",
      "stateful": false
    }
  ]
}

Now I can say "Hey Siri, turn on the office fan" and it works. The command flows from Siri -> HomeKit -> Homebridge -> TypeScript plugin -> Python script -> Flipper Zero -> IR/SubGHz signal -> device. Is it a Rube Goldberg machine? Absolutely. Does it work? Also yes.

Conclusion

With a combination of Homebridge, Flipper Zero, and some creative coding, you can bring almost any IR or SubGHz device into the Apple Home ecosystem. The key was bridging the gap between the TypeScript-based Homebridge plugin system and the Flipper Zero's serial interface using Python as the middle layer.

If you want to check out the code or build on it, the full project is available on GitHub.

The possibilities are endless -- any device with an IR remote or a SubGHz radio can now be controlled through Siri, automated with HomeKit scenes, and monitored through the Home app. Happy hacking!

Press K to Hire Me