Skip to main content
Glama
himnosiss

daikin-remote-mcp

by himnosiss
README.md
# daikin-remote-mcp — web panel remote and MCP server for Daikin air conditioners

Local control for Daikin units with a WiFi module, over the unit's own HTTP API.
No cloud, no Onecta account, no official app — everything stays on your network.

Two things sharing one client:

- **a web panel** you open in a browser to control the unit from your computer;
- **an MCP server**, so an assistant (Claude Code, Claude Desktop, …) can read the
  state and drive the unit through tools.

## Requirements

- Python 3.11+
- a Daikin unit whose WiFi module exposes the local HTTP API (see *Compatibility*)
- your machine on the same network as the unit

## Install

```bash
git clone <repo-url>
cd daikin-local
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt   # only needed for the MCP server
```

The web panel needs nothing installed — it runs on the standard library.

## Web panel

```bash
python3 web.py                    # http://127.0.0.1:8765
python3 web.py --port 9000
python3 web.py --bind 0.0.0.0     # reachable from the LAN, e.g. from a phone
```

Type the unit's IP address and press **Connect**. The address is saved to
`localStorage` and appears as a chip; `×` removes it from the list. You can keep
several units and switch between them. The list is mirrored into `devices.json`,
so the MCP server sees the same units.

The panel shows the state, room and outdoor temperature, instantaneous and daily
power use, and controls power, mode, temperature, fan, louvres and special modes.

**Why a local server rather than a plain `.html` file:** the Daikin module sends no
CORS headers. A static page could fire commands blind (`mode: 'no-cors'`) but could
never read back the state, the temperature or the power figures. `web.py` proxies
the requests and solves that.

## MCP server

```bash
claude mcp add daikin --scope user \
  --env DAIKIN_HOST=192.168.1.50 \
  -- /path/to/.venv/bin/python /path/to/server.py
```

Or, in any MCP client, through configuration:

```json
{
  "mcpServers": {
    "daikin": {
      "command": "/path/to/.venv/bin/python",
      "args": ["/path/to/server.py"],
      "env": { "DAIKIN_HOST": "192.168.1.50" }
    }
  }
}
```

Tools: `daikin_status`, `daikin_energy`, `daikin_set`, `daikin_set_special_mode`,
`daikin_list_devices`, `daikin_add_device`, `daikin_remove_device`, `daikin_discover`.

When `host` is omitted, the default unit from `devices.json` or `DAIKIN_HOST` is used.

## Layout

| File | What it does |
|---|---|
| `daikin/client.py` | Protocol client (standard library only) |
| `daikin/store.py` | Device list, shared by the panel and the MCP server |
| `daikin/debuglog.py` | Diagnostic logging, off by default |
| `server.py` | MCP server (stdio) |
| `web.py` | Local server: serves the page and proxies to the unit |
| `web/index.html` | The web panel |

## Troubleshooting

```bash
DAIKIN_DEBUG=1 python3 web.py
tail -f daikin.log
```

The log records every request to the unit and its raw response, plus events from the
page if you set `const DEBUG = true` in `web/index.html`. **Note:** the log contains
the unit's IP, MAC address, SSID and id, which is why it is in `.gitignore`.

## Protocol notes

Gathered by testing against a real unit (BRP069, `adp_kind=2`, firmware `4_0_3`).
There is no public documentation for this API, so most of what follows was worked out
empirically. Other modules or firmware versions may behave differently.

- The endpoints are plain `GET` requests with no authentication, returning
  comma-separated `key=value` pairs with percent-encoded values.
- `set_control_info` wants the **complete set** (`pow`, `mode`, `stemp`, `shum`,
  `f_rate`, `f_dir`) on every write and resets whatever is missing. Hence the
  read-modify-write in the client.
- Each mode remembers its own setpoint in `dt1..dt7`. When the mode changes without an
  explicit temperature, the client reuses the new mode's remembered value.
- Dry mode uses `dt2=M` (no numeric setpoint), and fan mode may have no `dt6` field at
  all — in those modes the temperature is not adjustable.
- Energy comes in units of **100Wh**. `datas` from `get_week_power` is a legacy field
  that does not correlate with the rest; use `get_week_power_ex` instead (index 0 =
  today). Verified by comparing against the hourly series from `get_day_power_ex`.
- **Special modes** are read from the `adv` field, separated by `/`. The write
  parameters are **not** the codes from `adv`:

  | Mode | Code in `adv` | How to write it |
  |---|---|---|
  | powerful (on) | `2` | `set_special_mode?spmode_kind=1&set_spmode=1` |
  | powerful (off) | | `set_special_mode?spmode_kind=1&set_spmode=0&en_powerful=0` |
  | econo | `12` | `set_special_mode?spmode_kind=2&set_spmode=0\|1` |
  | streamer | `13` | `set_special_mode?en_streamer=0\|1` |

  Switching powerful off **requires** `en_powerful=0` alongside `set_spmode=0`; without
  it the unit answers `ret=OK` and does nothing. `en_powerful` on its own returns
  `PARAM NG`.
- The unit **silently ignores** some special-mode commands: it answers `ret=OK` but
  `adv` does not change, especially when one command follows another closely. The client
  resends until `adv` confirms (up to 4 attempts).
- `powerful` and `econo` are mutually exclusive. `powerful` switches itself off after
  about 20 minutes, and opens the louvres wide while it is active.
- `ret=ADV NG` shows up occasionally on `set_control_info`. It was observed once, with
  powerful active and a changed setpoint. The hypothesis that "powerful locks the
  setpoint" **did not hold**: a later temperature change with powerful on went through
  fine. The client does not switch any mode off on its own — it reports the refusal and
  leaves the decision to the user.
- Error codes seen: `PARAM NG` (invalid or out-of-range parameter), `ADV NG` (conflict
  with a special mode).

## Compatibility

Works with modules that expose the local HTTP API: **BRP069A/B**, **BRP072A**,
**BRP15B61 "Airbase"**, **SKYFi**. The path prefix (`/` or `/skyfi`) is detected
automatically.

It does **not** work with BRP069C and newer, which use HTTPS with a token, nor with the
Onecta cloud. In that case the client returns an explicit message rather than a timeout.

## License

MIT — see [LICENSE](LICENSE).