Positivo PCUIRN MCP Server
README.md
# Casa Inteligente — Positivo PCUIRN
Control the **Positivo Smart Controle Universal 2** (PCUIRN) outside the official app: REST API, web UI, and MCP tools for Cursor / Claude Desktop.
The device is a Tuya-based IR blaster. This project reverse-engineers the **Positivo Casa Inteligente** mobile API (not Smart Life) and documents how IR commands reach your TV and air conditioner through the cloud.
> **Disclaimer:** This is an unofficial community project. It is not affiliated with Positivo or Tuya. Use at your own risk.
### TL;DR
1. **Setup:** `npm install` → copy `.env.example` → paste Positivo app **email + password** → configure MCP in Cursor (once)
2. **Use:** open Cursor chat and say *"liga o ar no 23"* or *"turn on the TV"* — done. No `npm start`, no device IDs, no Tuya Cloud.
---
## What works
| Feature | Method | Notes |
|---------|--------|-------|
| TV power, volume, channels, navigation | Cloud IR | Works without closing the mobile app |
| AC on/off, mode, temperature, fan | Cloud IR | Gree-style button keys (`M0_T23_S0`) |
| Device discovery | Positivo API / UDP scan | `positivo.js`, `discover.js` |
| Local LAN control | TuyAPI | Optional; only one client at a time |
| Cursor / Claude chat control | MCP server | `mcp/server.js` |
---
## Requirements
- Node.js 18+
- A Positivo Casa Inteligente account (same email/password as the mobile app)
- PCUIRN paired in the app with IR remotes configured (TV, AC, etc.)
---
## Setup (5 minutes)
### What you actually need
| `.env` variable | Required? | Used for |
|-----------------|-----------|----------|
| `POSITIVO_EMAIL` | **Yes** | Login to Positivo API |
| `POSITIVO_PASSWORD` | **Yes** | Login to Positivo API |
| `DEVICE_ID` | No | Local LAN only (`probe.js`, `/api/local/status`) |
| `DEVICE_LOCAL_KEY` | No | Local LAN only |
| `DEVICE_IP` | No | Local LAN only |
| `TUYA_ACCESS_ID` / `TUYA_ACCESS_SECRET` | No | Alternative way to get `localKey` — see [SETUP-TUYA.md](SETUP-TUYA.md) |
**Cloud control (TV, AC, API, web UI, MCP) only needs your Positivo app email and password.** No Tuya IoT Cloud developer account, no Smart Life, no `iot.tuya.com` project.
The PCUIRN must already be paired in the Positivo app with your IR remotes (TV, AC, etc.) configured.
### Quick start
```bash
git clone https://github.com/YOUR_USER/positivo-pcuirn.git
cd positivo-pcuirn
npm install
cp .env.example .env
```
Edit `.env` — only these two lines are required:
```env
POSITIVO_EMAIL=your@email.com
POSITIVO_PASSWORD=your_app_password
```
Then:
```bash
npm run build # install + build frontend (first time only)
npm start # API + UI → http://localhost:3000
```
Test from another terminal:
```bash
# Health check
curl http://localhost:3000/api/health
# List IR remotes (TV, AC…)
curl http://localhost:3000/api/remotes
# Turn on TV
curl -X POST http://localhost:3000/api/tv/send \
-H "Content-Type: application/json" \
-d '{"command":"power"}'
# AC at 23°C (cool, auto fan)
curl -X POST http://localhost:3000/api/ac/send \
-H "Content-Type: application/json" \
-d '{"action":"set","mode":0,"temp":23,"fan":0}'
```
On first run, login happens automatically and the session is saved to `storage/session.json` (gitignored). You don't need to run any other setup script.
### Optional: device ID / localKey (local LAN only)
**Skip this** if you only use cloud control (TV, AC, Cursor, web UI).
One command does everything — login, list devices, print `.env` lines, save cache:
```bash
node positivo.js
```
Look for **Smart Controle Universal 2** (`productId: lwpag3bu0faaowlj`). Copy the printed lines into `.env` only if you need local control (`probe.js`).
If IP is missing, run `node discover.js` to find it on your Wi-Fi.
You do **not** need Tuya IoT Cloud (`iot.tuya.com`) for normal use — see [SETUP-TUYA.md](SETUP-TUYA.md) only as a last resort.
### Cursor MCP (one-time)
1. **Cursor Settings → MCP** → add server (see `mcp/cursor-mcp.example.json`)
2. Set the **absolute path** to `mcp/server.js`
3. Restart Cursor
The MCP server uses the same `.env` (email + password). **`npm start` does not need to be running** for Cursor control.
### Development (split API + UI)
```bash
# Terminal 1
npm run dev
# Terminal 2
npm run frontend:dev # http://localhost:5173 (proxies to API)
```
---
## How to use
After setup, pick how you want to control things. **No device IDs, no curl, no Tuya Cloud** required for the options below.
### 1. Cursor / Claude — just ask (recommended)
With MCP configured, talk to the AI in normal language. It calls the tools and sends IR commands over the cloud.
**You don't need `npm start` running.** Only `.env` with email + password.
Examples (Portuguese or English — both work):
| You say | What happens |
|---------|----------------|
| *"Liga o ar no 23"* | AC on, cool mode, 23°C |
| *"Desliga o ar"* | AC off |
| *"Coloca o ar em 16 graus"* | Sets temperature to 16°C |
| *"Liga a TV"* | TV power |
| *"Aumenta o volume da TV"* | Volume up |
| *"Turn on the AC at 23 degrees"* | Same as above |
| *"Mute the TV"* | TV mute |
The agent picks the right MCP tool (`ac_set_temperature`, `tv_power`, etc.) automatically. You never type tool names yourself.
If something fails, check that MCP is enabled in Cursor and `.env` has the correct Positivo credentials.
### 2. Web UI
```bash
npm start
```
Open **http://localhost:3000** — buttons for TV and AC (mode, temperature, fan).
### 3. API / curl / scripts
With `npm start` running:
```bash
curl -X POST http://localhost:3000/api/ac/send \
-H "Content-Type: application/json" \
-d '{"action":"set","mode":0,"temp":23,"fan":0}'
```
Or without the server:
```bash
node test-control.js send TV power
node test-control.js send "Ar condicionado" "power off"
```
---
## How it was built (reverse engineering)
### 1. The hardware
| Item | Value |
|------|-------|
| Product | Positivo Smart Controle Universal 2 |
| Internal name | PCUIRN |
| Tuya `productId` | `lwpag3bu0faaowlj` |
| Role | IR gateway — exposes virtual remotes (TV, AC) as Tuya sub-devices |
The gateway stores learned IR codes. Virtual remotes appear as separate devices under your account.
### 2. Positivo uses Tuya, but not Smart Life
The official app talks to Tuya's **mobile API** at `https://a1.tuyaus.com/api.json` (US region). Authentication uses the same account as the Positivo app — **not** a Tuya IoT Cloud developer project.
App credentials (`clientId`, signing secret, virtual `deviceId`) were extracted from the Positivo Casa Inteligente APK. They live in `positivo.js` and are **app-level constants**, not user secrets. Your personal credentials go only in `.env`.
### 3. Request signing
Every API call is signed with HMAC-SHA256. The implementation in `positivo.js` follows the Tuya mobile SDK:
1. Sort selected query parameters
2. Hash `postData` with a custom MD5 shuffle (`mobileHash`)
3. Build `key=value||key=value` string
4. Sign with the app secret
Password login uses RSA encryption of the MD5-hashed password (`encPassword`), matching Tuya's mobile flow.
### 4. Session management
After login, a `sid` (session id) is cached in `storage/session.json` (gitignored). The client revalidates the session and re-logins when it expires.
### 5. Listing devices
```
tuya.m.location.list
→ tuya.m.my.group.device.list (per home/group)
→ tuya.m.device.get (per device → deviceId, localKey, ip, productId)
```
`node positivo.js` prints everything you need for `.env` and writes `devices.json` locally (also gitignored).
### 6. IR remotes and buttons
IR remotes are sub-devices of the gateway:
```
tuya.m.device.sub.list # list remotes (TV, AC, …)
tuya.m.infrared.record.get # remote metadata
tuya.m.infrared.keydata.get # button list + compressed IR payloads
tuya.m.device.dp.publish # send IR (DPS payload)
```
Important constants discovered:
| Constant | Value | Meaning |
|----------|-------|---------|
| `IR_VENDOR` | `3` | Vendor code for Positivo IR payloads |
| Gateway `productId` | `lwpag3bu0faaowlj` | Used to auto-detect the PCUIRN |
Each button maps to a DPS object. Learned buttons use `study_key`; preloaded Gree AC codes use `send_ir` with a compressed pulse and extension field `99999`.
### 7. Gree air conditioner key format
The AC remote uses Tuya's standard Gree encoding. Button names follow:
```
M{mode}_T{temp}_S{fan} # cool/heat/auto/dry
M2_S{fan} # fan-only mode (no temperature)
```
| Mode | Value |
|------|-------|
| Cool | `0` |
| Heat | `1` |
| Fan | `2` |
| Auto | `3` |
| Dry | `4` |
Examples: `M0_T23_S0` = cool 23°C auto fan, `power on` / `power off` for toggle.
This mapping is in `lib/commands.js`.
### 8. Cloud vs local control
**Cloud (recommended):** `tuya.m.device.dp.publish` via the Positivo API. The gateway receives the command over the internet. No need to close the mobile app.
**Local (optional):** Direct TCP to the device with [TuyAPI](https://github.com/codetheweb/tuyapi) using `DEVICE_ID`, `DEVICE_LOCAL_KEY`, and `DEVICE_IP`. Only one connection at a time — close the app first. Test with `node probe.js`.
**LAN discovery:** `node discover.js` broadcasts on Tuya UDP ports `6666`/`6667` to find devices on your Wi-Fi.
### 9. Alternative: Tuya IoT Cloud (optional, rarely needed)
Only for **local LAN** control when `node positivo.js` does not return a `localKey`. Cloud IR does **not** use this. See [SETUP-TUYA.md](SETUP-TUYA.md).
---
## REST API
Base URL: `http://localhost:3000`
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/health` | Health check |
| GET | `/api/devices` | List account devices |
| GET | `/api/commands` | TV/AC command catalog |
| GET | `/api/remotes` | IR remotes on the gateway |
| GET | `/api/remotes/:name/buttons` | Buttons for a remote |
| POST | `/api/tv/send` | Send TV command (`{ "command": "power" }`) |
| POST | `/api/ac/send` | Send AC action |
| POST | `/api/ir/send` | Raw IR (`{ "remote": "TV", "button": "power" }`) |
| GET | `/api/local/status` | Local DPS via TuyAPI |
### Examples
```bash
# List remotes
curl http://localhost:3000/api/remotes
# TV power
curl -X POST http://localhost:3000/api/tv/send \
-H "Content-Type: application/json" \
-d '{"command":"power"}'
# AC: cool mode, 23°C, auto fan
curl -X POST http://localhost:3000/api/ac/send \
-H "Content-Type: application/json" \
-d '{"action":"set","mode":0,"temp":23,"fan":0}'
# AC off
curl -X POST http://localhost:3000/api/ac/send \
-H "Content-Type: application/json" \
-d '{"action":"off"}'
```
---
## Web UI
See [How to use → Web UI](#2-web-ui).
---
## MCP server (reference)
Tools exposed to Cursor / Claude Desktop. For day-to-day use, just ask in chat — see [How to use → Cursor](#1-cursor--claude--just-ask-recommended).
### Tools
| Tool | Action |
|------|--------|
| `tv_power` | TV power toggle |
| `tv_volume_up` / `tv_volume_down` | Volume |
| `tv_mute` | Mute |
| `tv_channel_up` / `tv_channel_down` | Channels |
| `tv_ok` / `tv_menu` | Navigation |
| `tv_send` | Any catalog command by id |
| `ac_power_on` / `ac_power_off` | AC power |
| `ac_set` | Mode + temp + fan (numeric) |
| `ac_set_temperature` | Set temp (cool mode) |
| `ac_set_mode` | Set mode by name |
| `list_commands` | Show available commands |
### Cursor config
```json
{
"mcpServers": {
"casa-inteligente": {
"command": "node",
"args": ["/absolute/path/to/positivo-pcuirn/mcp/server.js"]
}
}
}
```
### Claude Desktop
Same config in `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS).
---
## CLI scripts
| Script | Purpose |
|--------|---------|
| `node positivo.js` | Login + list devices, write `devices.json` |
| `node positivo.js --login` | Force fresh login |
| `node discover.js` | UDP scan for Tuya devices on LAN |
| `node probe.js` | Test local TuyAPI connection |
| `node cloud.js` | List devices via Tuya IoT Cloud |
| `node test-control.js list` | List devices (service layer) |
| `node test-control.js send TV power` | Send IR via cloud |
| `npm run mcp` | Run MCP server (stdio) |
---
## Project structure
```
lib/
positivo-client.js # Tuya mobile API + IR layer
commands.js # TV/AC command catalog (Gree keys)
service.js # Shared service for API + MCP
local-device.js # TuyAPI local connection
positivo.js # Login, signing, session cache
src/server.js # Express API + static UI
mcp/server.js # MCP stdio server
frontend/ # React UI
storage/session.json # Cached login session (gitignored)
devices.json # Device cache from positivo.js (gitignored)
```
---
## Security & secrets
**Never commit:**
| File | Contains |
|------|----------|
| `.env` | Your email, password, device keys |
| `storage/session.json` | Active API session |
| `devices.json` | Device IDs, local keys, IPs |
| `.cursor/mcp.json` | Local IDE paths |
Copy `.env.example` → `.env` and use `devices.example.json` as a reference for the cache format.
If `localKey` values were ever committed or shared, rotate them by removing and re-adding the device in the Positivo app, then re-run `node positivo.js`.
The `CLIENT_ID` / `SECRET_KEY` in `positivo.js` are embedded in the public APK — they identify the app, not your account.
---
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues