Local Home‑Automation Bridge for Samsung Appliances (Mini‑Clone)

A 45‑minute beginner guide to a local‑only Samsung appliance bridge

Welcome to another official UVF IT build guide! In this tutorial you’ll create a tiny, self‑hosted service that mimics the core of the LocalThings integration – a simple HTTP API that discovers a Samsung appliance by IP and exposes its power state.

We’ll use Python 3, FastAPI, and SQLite (all free‑tier) to spin up a local bridge you can run on any laptop or Raspberry Pi. No cloud keys, no paid SDKs – just pure local networking.

Before you start…

Mini‑Clone Architecture
LOCAL RUNTIME reads/writes HTTP/TCP commands FastAPI Bridge Python code SQLite DB State store Samsung Appliance Local network device
1

Phase 1: Scaffold the project

First we create an isolated Python environment so our dependencies don’t clash with any other projects on the machine. Then we install FastAPI – the web framework that will expose our tiny API – and Uvicorn, the ASGI server that runs it. Finally we create an empty SQLite file; FastAPI will use it later to persist the appliance’s power state.

All of these steps are one‑liners, but keeping them together in a script makes the clone reproducible on any system you later move to, such as a Raspberry Pi.

# Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate

# Upgrade pip and install core dependencies
pip install --upgrade pip
pip install fastapi uvicorn[standard]

# Initialise an empty SQLite database file
sqlite3 appliance.db "VACUUM;"
2

Phase 2: Model – store appliance info

We need a clear contract for the data we’ll receive and store. A Pydantic model gives us type‑checked validation for incoming JSON payloads, while a tiny helper function abstracts the SQLite upsert logic – inserting a new row or updating the existing one based on the appliance’s unique IP address.

Keeping the model and DB helper in a dedicated models.py file isolates the data layer from the API routes, which makes the code easier to test and extend later (for example, adding temperature readings).

models.py

from pydantic import BaseModel, Field
import sqlite3
from typing import Optional

# -------------------------------------------------------------------
# Pydantic schema – describes the JSON body the API expects
# -------------------------------------------------------------------
class Device(BaseModel):
    ip: str = Field(..., description="Local IP address of the Samsung appliance")
    name: Optional[str] = Field(None, description="Friendly name for the device")
    power: bool = Field(..., description="Current power state; True=on, False=off")

# -------------------------------------------------------------------
# SQLite helper – creates table if missing and upserts a device record
# -------------------------------------------------------------------
_DB_PATH = "appliance.db"

def _ensure_table():
    with sqlite3.connect(_DB_PATH) as conn:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS devices (
                ip TEXT PRIMARY KEY,
                name TEXT,
                power INTEGER NOT NULL
            );
            """
        )
        conn.commit()

def upsert_device(device: Device):
    """Insert a new device or update the existing row identified by IP."""
    _ensure_table()
    with sqlite3.connect(_DB_PATH) as conn:
        conn.execute(
            """
            INSERT INTO devices (ip, name, power) VALUES (?, ?, ?)
            ON CONFLICT(ip) DO UPDATE SET name=excluded.name, power=excluded.power;
            """,
            (device.ip, device.name, int(device.power)),
        )
        conn.commit()
3

Phase 3: API – discover & control

In this step we turn our tiny SQLite store into a real HTTP API using FastAPI. The /discover endpoint accepts a JSON body with an ip field and writes it to the device table – no authentication is needed because this clone runs only on your local network. The /power endpoint reads the stored IP, pretends to send a TCP command, flips a simulated power flag, and returns the new state as JSON. Keeping the logic tiny makes it easy to see how the real LocalThings bridge talks to Samsung appliances without the complexity of encryption.

main.py

import json
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import aiosqlite
import asyncio

app = FastAPI()
DB = 'appliances.db'

class Device(BaseModel):
    ip: str

async def init_db():
    async with aiosqlite.connect(DB) as db:
        await db.execute('CREATE TABLE IF NOT EXISTS device (ip TEXT PRIMARY KEY, power INTEGER DEFAULT 0)')
        await db.commit()

@app.on_event('startup')
async def startup():
    await init_db()

@app.post('/discover')
async def discover(dev: Device):
    async with aiosqlite.connect(DB) as db:
        try:
            await db.execute('INSERT INTO device (ip) VALUES (?)', (dev.ip,))
            await db.commit()
        except aiosqlite.IntegrityError:
            raise HTTPException(status_code=400, detail='Device already registered')
    return {'status': 'registered', 'ip': dev.ip}

@app.post('/power')
async def power(dev: Device):
    async with aiosqlite.connect(DB) as db:
        cur = await db.execute('SELECT power FROM device WHERE ip = ?', (dev.ip,))
        row = await cur.fetchone()
        if not row:
            raise HTTPException(status_code=404, detail='Device not found')
        new_state = 0 if row[0] else 1  # toggle
        await db.execute('UPDATE device SET power = ? WHERE ip = ?', (new_state, dev.ip))
        await db.commit()
    # Simulate TCP command (no real network traffic)
    await asyncio.sleep(0.1)
    return {'ip': dev.ip, 'power': new_state}
4

Phase 4: Run locally

Now that the API code is ready, we launch it with Uvicorn. Running the server in the background lets you keep using the same terminal for the next steps, while the virtual environment stays active so imports resolve correctly.

# Ensure the virtualenv is active
source venv/bin/activate
# Start FastAPI with Uvicorn in the background, listening on port 8000
nohup uvicorn main:app --host 0.0.0.0 --port 8000 > server.log 2>&1 &
# Print the PID so you can stop it later
echo "Server PID: $!"
5

Phase 5: Verify & extend

With the server running we can interact using curl. First we register a fake appliance IP, then we toggle its power state and watch the JSON response. Once the basic flow works you can easily add more fields (temperature, mode) to the SQLite schema or swap the database for PostgreSQL by changing the init_db and query sections.

# Register a dummy device (replace 192.168.1.42 with your appliance IP)
curl -s -X POST http://localhost:8000/discover -H 'Content-Type: application/json' -d '{"ip":"192.168.1.42"}'

# Toggle power – first call turns it ON, second call turns it OFF
curl -s -X POST http://localhost:8000/power -H 'Content-Type: application/json' -d '{"ip":"192.168.1.42"}'
curl -s -X POST http://localhost:8000/power -H 'Content-Type: application/json' -d '{"ip":"192.168.1.42"}'

# Optional: view the SQLite file directly
sqlite3 appliances.db "SELECT ip, power FROM device;"

Verifying the Stack

CheckCommand / Why it matters
Virtualenv activated and dependencies installedpip list | grep -E 'fastapi|uvicorn|aiosqlite'
SQLite file contains a row after discoverysqlite3 appliances.db "SELECT * FROM device;"
API returns JSON on power togglecurl -s -X POST http://localhost:8000/power -H 'Content-Type: application/json' -d '{"ip":"192.168.1.42"}' | jq .

What’s next?

You now have a functional local bridge. To approach the full LocalThings feature set, add encrypted SmartThings protocol handling, periodic polling, and Home Assistant MQTT discovery. Happy hacking!