From d438da752d526c4912d67fef0e993a17bbf824e3 Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Tue, 21 Apr 2026 23:58:54 +0100 Subject: [PATCH 1/6] docs(readme): overhaul with badges, demo, architecture; add privacy policy - Centered tagline + 6 shields (PyPI, Python, CI, Coverage, License, Privacy) - Add "What is iotcli?" section with key principles - Add Demo section with list/status/JSON examples - Add Architecture overview with directory tree - Expand AI Agent Integration with clearer MCP/skill/workflow sections - Add PRIVACY.md: local-only data, Fernet encryption, no telemetry - Fix duplicate device lookup line in control.py (post-merge artifact) --- PRIVACY.md | 58 +++++++++ README.md | 192 ++++++++++++++++++++--------- src/iotcli/cli/commands/control.py | 1 - 3 files changed, 194 insertions(+), 57 deletions(-) create mode 100644 PRIVACY.md diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..4531d73 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,58 @@ +# Privacy Policy + +**Effective date:** 2026-04-21 + +This Privacy Policy describes how the **iotcli** open-source project ("we", "us", or "our") handles information. + +## 1. Overview + +iotcli is a local command-line tool for controlling IoT devices. It does not operate any central service, does not collect telemetry, and does not transmit your personal data to project maintainers. + +## 2. Information Stored Locally + +All data created by iotcli is stored locally on your machine in `~/.iotcli/`: + +| File / Directory | Contents | +|------------------|----------| +| `devices.yaml` | Device names, protocols, IP addresses, and non-sensitive settings | +| `credentials/*.enc` | Fernet-encrypted secrets (tokens, local keys, passwords) | +| `.key` | Local-only Fernet encryption key (permissions `0600`) | +| `skills/` | Generated AI-agent skill files based on your device configuration | + +No project maintainer, server, or third party (other than the device vendor you are intentionally communicating with) has access to these files. + +## 3. Third-Party Services + +When you use iotcli to communicate with a device, data is transmitted directly between your machine and the device or its vendor cloud (e.g., Tuya cloud, LG ThinQ). This communication is: + +- **Initiated by you** via explicit CLI commands. +- ** Governed by the device vendor's privacy policy**, not this one. + +We do not act as an intermediary, proxy, or data processor for these communications. + +## 4. No Telemetry or Analytics + +iotcli does not: + +- Collect usage statistics or crash reports. +- Use tracking cookies or fingerprinting. +- Send data to external analytics services. +- Display advertisements. + +## 5. Data Security + +- Credentials are encrypted at rest using Fernet (symmetric AES-128 in CBC mode with HMAC). +- The encryption key is stored with owner-only read permissions (`0600`). +- We recommend you keep your operating system and Python environment up to date. + +## 6. Open Source + +The full source code is available at https://github.com/fomyio/iotcli. You can inspect exactly what the tool does and how it handles data. + +## 7. Changes to This Policy + +We may update this policy to reflect changes in the software. The latest version will always be available in the repository. + +## 8. Contact + +For privacy questions, open an issue at https://github.com/fomyio/iotcli/issues. diff --git a/README.md b/README.md index 31ae4f7..fb2c1a4 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,56 @@ # iotcli -**Give your AI agent hands — control any smart home device from the terminal.** - -One CLI to rule them all. Discover, configure, and control IoT devices across protocols. Built for AI agents, loved by humans. - -```text -$ iotcli control status lg-ac -╭─── lg-ac — online ───╮ -│ power: POWER_OFF │ -│ mode: HEAT │ -│ fan_speed: HIGH │ -│ current_temp: 20.5 │ -│ target_temp: 25 │ -╰──────────────────────╯ -``` +

+ Give your AI agent hands.
+ One CLI to rule them all. Discover, configure, and control IoT devices across protocols.
+ Built for AI agents, loved by humans. +

+ +

+ PyPI version + Python versions + CI + Coverage + License + Privacy +

+ +

+ Install • + Quick Start • + AI Integration • + Protocols • + Architecture +

+ +--- + +## What is iotcli? + +iotcli is a universal IoT device control CLI. It unifies smart-home devices behind a single, predictable interface — so you (or your AI agent) can control lights, AC units, feeders, and sensors without memorizing protocol quirks. + +**Key principles:** +- **One command shape** for every device — `on`, `off`, `status`, `set` +- **Protocol-agnostic** — Tuya, miIO, MQTT, HTTP, LG ThinQ, and more +- **AI-native** — JSON mode, skill files, and an MCP server for Claude Desktop / Cursor / Zed +- **Privacy-first** — everything local, credentials encrypted, no telemetry ## Install ```bash -# Global install (recommended — no venv needed, no package conflicts) -pipx install git+https://github.com/joeVenner/CLI-IoT.git +# Global install via pipx (recommended — no venv needed) +pipx install git+https://github.com/fomyio/iotcli.git + +# Or with MCP support for Claude Desktop / Cursor +pipx install git+https://github.com/fomyio/iotcli.git[mcp] -# Or from a local clone -git clone https://github.com/joeVenner/CLI-IoT.git -cd CLI-IoT -pipx install . +# From source +git clone https://github.com/fomyio/iotcli.git +cd iotcli +pip install -e ".[dev]" ``` -After install, `iotcli` is available system-wide. No activation, no venv. +> Requires Python 3.10+ ## Quick Start @@ -36,9 +59,9 @@ After install, `iotcli` is available system-wide. No activation, no venv. iotcli setup # Discover devices on your network -iotcli discover +iotcli discover --network 192.168.1.0/24 -# Add a device non-interactively +# Add a device iotcli add --name living-room-light --protocol miio \ --ip 192.168.1.100 --token <32chars> @@ -53,24 +76,48 @@ iotcli --json control status living-room-light iotcli --json status-all ``` +## Demo + +```text +$ iotcli list +╭─────────── Devices ───────────╮ +│ Name Protocol IP │ +│ living-room-light miio 192.168.1.100 │ +│ feeder tuya 192.168.1.4 │ +│ lg-ac lgac (cloud) │ +╰───────────────────────────────╯ + +$ iotcli control status lg-ac +╭─── lg-ac — online ───╮ +│ power: POWER_OFF │ +│ mode: HEAT │ +│ fan_speed: HIGH │ +│ current_temp: 20.5 │ +│ target_temp: 25 │ +╰──────────────────────╯ + +$ iotcli --json control on feeder +{"success": true, "device": "feeder", "action": "on"} +``` + ## Supported Protocols -| Protocol | Devices | Connection | -| -------- | -------------------------------- | ------------- | -| `miio` | Xiaomi / Yeelight | Local (UDP) | -| `tuya` | Tuya-based (lights, plugs, etc.) | Local (TCP) | -| `mqtt` | Zigbee / Aqara via MQTT broker | Local (TCP) | -| `http` | ESPHome / Tasmota | Local (HTTP) | -| `lgac` | LG Air Conditioner (ThinQ) | Cloud (HTTPS) | +| Protocol | Devices | Connection | +|----------|---------|------------| +| `miio` | Xiaomi / Yeelight | Local (UDP) | +| `tuya` | Tuya-based (lights, plugs, etc.) | Local (TCP) | +| `mqtt` | Zigbee / Aqara via MQTT broker | Local (TCP) | +| `http` | ESPHome / Tasmota | Local (HTTP) | +| `lgac` | LG Air Conditioner (ThinQ) | Cloud (HTTPS) | -### Tuya Device Profiles +### Tuya Profiles -| Profile | Devices | Special Actions | -| ------------ | --------------------- | -------------------------------------- | -| `generic` | Any Tuya device | power on/off | -| `light` | Smart bulbs | brightness, color_temperature, color | -| `switch` | Smart plugs | power, countdown | -| `petfeeder` | ROJECO / Tuya feeders | portions, quick_feed, slow_feed, light | +| Profile | Devices | Special Actions | +|---------|---------|-----------------| +| `generic` | Any Tuya device | power on/off | +| `light` | Smart bulbs | brightness, color_temperature, color | +| `switch` | Smart plugs | power, countdown | +| `petfeeder` | ROJECO / Tuya feeders | portions, quick_feed, slow_feed, light | ```bash iotcli add --name feeder --protocol tuya --profile petfeeder \ @@ -79,32 +126,35 @@ iotcli add --name feeder --protocol tuya --profile petfeeder \ ## AI Agent Integration -Every command supports `--json` for structured output. Feed your agent the generated skill files and it knows how to control your home. +iotcli was built from the ground up for AI agents. Every command supports `--json` for structured, parseable output. + +### Skill Files + +Generate per-device skill files so your agent knows exactly what each device can do: ```bash -# Generate skill files for all configured devices iotcli skills generate - -# Files created in ~/.iotcli/skills/: -# iotcli.skill.yaml — global tool spec with all devices -# system_prompt.md — ready-to-use agent system prompt -# /SKILL.md — per-device capability doc -# iotcli.tools.json — OpenAI/Anthropic tool schema ``` +Files created in `~/.iotcli/skills/`: +- `/SKILL.md` — per-device capability doc (OpenClaw-compatible) +- `iotcli.tools.json` — OpenAI/Anthropic tool schema +- `iotcli.skill.yaml` — legacy global skill spec +- `system_prompt.md` — ready-to-use agent system prompt + ### MCP Server (Claude Desktop / Cursor / Zed) -iotcli ships an MCP server so Claude can control your devices directly in conversation — no copy-pasting commands. +Connect iotcli directly to your AI assistant via the Model Context Protocol: ```bash # Install with MCP support pip install iotcli[mcp] -# Start the server (Claude Desktop launches this for you) +# Start the server iotcli serve mcp ``` -Add to your Claude Desktop config (`claude_desktop_config.json`): +Add to `claude_desktop_config.json`: ```json { @@ -117,9 +167,9 @@ Add to your Claude Desktop config (`claude_desktop_config.json`): } ``` -Once connected, Claude can list devices, check status, turn them on/off, and set properties — all through structured tool calls with per-device enums and value validation. +Once connected, Claude can list devices, check status, turn them on/off, and set properties — all through structured tool calls with per-device enums and value validation. No copy-pasting commands. -### Agent Workflow (CLI) +### Agent Workflow ```bash # 1. Discover what's available @@ -133,6 +183,36 @@ iotcli --json control on "living-room-light" iotcli --json control set "living-room-light" "brightness=80" ``` +## Architecture + +```text +src/iotcli/ +├── core/ # Device model, protocol registry, controller +├── protocols/ # Self-registering protocol handlers +├── config/ # YAML config + Fernet credential vault +├── discovery/ # Async multi-protocol network scanner +├── tui/ # Rich + InquirerPy interactive wizard +├── cli/ # Click commands (discover, control, device, config, skills) +├── skills/ # Jinja2-based AI agent skill generator +└── mcp/ # MCP server for Claude Desktop / Cursor / Zed +``` + +**Design decisions:** +- **Protocol registry** (`core/registry.py`): Decorator-based self-registration. No hardcoded mappings. +- **Device model** (`core/device.py`): Dataclass with `slugify()` for normalized names. +- **Credentials**: Fernet-encrypted vault, never in `devices.yaml`. Key stored at `0600`. +- **Extensible**: Add a protocol by implementing `BaseProtocol` — the CLI, wizard, and skill generator pick it up automatically. + +## Security + +- Credentials are **never** stored in config files +- Secrets encrypted with Fernet at `~/.iotcli/credentials/` +- Encryption key has `0600` permissions (owner-only) +- No telemetry, no analytics, no cloud dependency +- Fully open source — inspect exactly what runs on your machine + +See [PRIVACY.md](PRIVACY.md) for details. + ## Extending — Add a New Protocol ```python @@ -158,12 +238,12 @@ class MyProtocol(BaseProtocol): Add one import in `protocols/__init__.py` — the CLI, wizard, and skill generator pick it up automatically. -## Security - -- Credentials are **never** stored in config files -- Secrets encrypted with Fernet at `~/.iotcli/credentials/` -- Encryption key has `0600` permissions (owner-only) - ## License MIT + +--- + +

+ Made with passion for AI agents and smart homes. +

diff --git a/src/iotcli/cli/commands/control.py b/src/iotcli/cli/commands/control.py index 5a76970..50b8c93 100644 --- a/src/iotcli/cli/commands/control.py +++ b/src/iotcli/cli/commands/control.py @@ -38,7 +38,6 @@ def control(ctx, action, device_name, value): out = Output(ctx.obj["json_output"]) cfg: ConfigManager = ctx.obj["config"] - device = cfg.get_device_or_none(device_name) device = cfg.get_device_or_none(device_name) if not device: out.error(f"Device not found: {device_name}") From 166eef20e5290680f0d23747986a3593c86ce5fd Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Wed, 22 Apr 2026 14:47:06 +0100 Subject: [PATCH 2/6] feat(ci): add PyPI publish workflow --- .github/workflows/publish.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..877fa49 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +name: Publish to PyPI + +on: + push: + tags: + - "v*" + +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: pip install build + + - name: Build wheel and sdist + run: python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} From ed93eacd2b79c9be58c39e0d22f748a4b928c843 Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Wed, 22 Apr 2026 14:54:27 +0100 Subject: [PATCH 3/6] fix(ci): address greptile review - pin action SHA and remove unused id-token perm --- .github/workflows/publish.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 877fa49..3224c8f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write steps: - name: Checkout uses: actions/checkout@v4 @@ -27,6 +26,6 @@ jobs: run: python -m build - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: password: ${{ secrets.PYPI_API_TOKEN }} From 6e3589729e067b9871d82fe2bb667eb528c59176 Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Wed, 22 Apr 2026 15:32:13 +0100 Subject: [PATCH 4/6] feat(ci): switch to Trusted Publishing (OIDC) + pypi environment protection --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3224c8f..c7b4b88 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,8 +8,10 @@ on: jobs: build-and-publish: runs-on: ubuntu-latest + environment: pypi permissions: contents: read + id-token: write steps: - name: Checkout uses: actions/checkout@v4 @@ -25,7 +27,5 @@ jobs: - name: Build wheel and sdist run: python -m build - - name: Publish to PyPI + - name: Publish to PyPI (Trusted Publishing) uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - password: ${{ secrets.PYPI_API_TOKEN }} From 8de4de67712d8595ad3edecfa692194836674cab Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Wed, 22 Apr 2026 15:51:48 +0100 Subject: [PATCH 5/6] fix(ci): add actor gate and dynamic version from tag --- .github/workflows/publish.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c7b4b88..b72354f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,7 @@ on: jobs: build-and-publish: + if: github.actor == 'joeVenner' runs-on: ubuntu-latest environment: pypi permissions: @@ -16,6 +17,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set version from tag + run: | + TAG=${GITHUB_REF_NAME#v} + sed -i "s/^version = .*/version = \"$TAG\"/" pyproject.toml + - name: Set up Python uses: actions/setup-python@v5 with: From 73b0e1a390c363b8e717ce4c5d9f3fbdebc0007b Mon Sep 17 00:00:00 2001 From: JoeVenner Date: Wed, 22 Apr 2026 16:01:07 +0100 Subject: [PATCH 6/6] =?UTF-8?q?fix(ci):=20remove=20hardcoded=20actor=20gua?= =?UTF-8?q?rd=20=E2=80=94=20rely=20on=20tag=20ruleset=20+=20env=20approval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b72354f..436e9df 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,6 @@ on: jobs: build-and-publish: - if: github.actor == 'joeVenner' runs-on: ubuntu-latest environment: pypi permissions: