File size: 6,425 Bytes
664512d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | # Ecowitt Cloud API v3 β Notes from Live Calls
Source: live calls against my GW3000B on 2026-05-10. Cross-checked against
[doc.ecowitt.net](https://doc.ecowitt.net/web/#/apiv3en?page_id=1) (SPA β not
fetchable; learnings here come from the actual responses).
## Auth
Every request takes three query params:
- `application_key` β from User Center β Private Center β API Keys
- `api_key` β same place
- `mac` β device MAC, e.g. `6C:C8:40:38:94:DB`
No headers required. HTTPS only.
## Base URL
`https://api.ecowitt.net/api/v3`
## Response envelope
Every response is wrapped:
```json
{ "code": 0, "msg": "success", "time": "1778435131", "data": { ... } }
```
`code != 0` means error; `msg` carries the reason. `time` is server unix seconds (string).
## Endpoints we use
### `GET /device/info`
Sanity check + a real-time snapshot. No `call_back` needed. Returns device
metadata (`name`, `mac`, `date_zone_id`, lat/lon, `stationtype`, `device_status`)
and a `last_update` block whose shape matches the real-time payload below.
### `GET /device/real_time`
Current snapshot. Required: auth + `call_back`. Real-time leaves have shape
`{time: unix_str, unit: str, value: str}` β different from history (see below).
### `GET /device/history`
Historical series. Required params:
- auth (3 keys)
- `start_date`, `end_date` β `YYYY-MM-DD HH:MM:SS`. We send naive UTC; the API
returns unix seconds that align with UTC, so no tz conversion needed despite
the device tz being `America/New_York`.
- `cycle_type` β see below
- `call_back` β comma-separated channel list. `all` is **rejected** for
`/device/history` (`40016 all is invalid`) even though it works for
`/device/real_time`. Pass explicit channels like
`outdoor,indoor,pressure,wind,solar_and_uvi,rainfall_piezo,rainfall,battery`.
#### `cycle_type` values + storage tiers
Ecowitt retains data at three resolutions:
| cycle_type | resolution | retention |
|------------|------------|-----------|
| `5min` | 5 minutes | last 90 days |
| `30min` | 30 minutes | last 1 year |
| `4hour` | 4 hours | last 2 years |
`auto` is also accepted and picks resolution based on the requested range.
**Probed empirically** β the docs *suggest* names like `240min`, but those
return `40015 Invalid cycle_type`. The four valid values are `5min`, `30min`,
`4hour`, and `auto`. Anything else is rejected.
#### History response shape
Different from real_time β values are stored in a `list` map keyed by unix seconds:
```json
{
"data": {
"outdoor": {
"temperature": {
"unit": "ΒΊF",
"list": {
"1778277600": "57.1",
"1778279400": "56.8",
...
}
},
"humidity": { "unit": "%", "list": { ... } },
...
},
"pressure": { "relative": { ... }, "absolute": { ... } },
"wind": { "wind_speed": { ... }, ... },
"outdoor": { "temperature": { ... }, "humidity": { ... }, ... },
"indoor": { ... },
"solar_and_uvi": { "solar": { ... }, "uvi": { ... } },
"rainfall_piezo": { "rain_rate": { ... }, ... },
"battery": { "haptic_array_battery": { ... }, ... }
}
}
```
Every metric leaf is `{ unit: str, list: { unix_seconds_str: value_str } }`. Both
the timestamp keys and the values are strings β cast on the way in.
The structure is consistently 2 levels deep: `data.{channel}.{metric}` for the
metrics we've seen on this station. `pressure` only has `relative` and
`absolute`; everything else has multiple metrics.
## Running the SQLite sync
The archive lives at `data/ecowitt.db` (gitignored). The sync is idempotent:
re-running is safe, and only new rows are written.
```bash
# full update across all three cycle_types (5min, 30min, 4hour)
.venv/bin/python -m src.sync
# just one cycle_type
.venv/bin/python -m src.sync --cycle 5min
# different DB path
.venv/bin/python -m src.sync --db data/other.db
# bigger overlap when resuming (default is 24h)
.venv/bin/python -m src.sync --overlap-hours 48
```
How "incremental" works: for each `cycle_type` the sync queries
`MAX(ts_unix)` from the DB and starts the next fetch at `max_ts β overlap`
(default 1 day). Overlap re-fetches a small tail to catch late-arriving
points; dedup is handled by the primary key
`(cycle_type, channel, metric, ts_unix)` with `INSERT OR REPLACE`.
Suggested cadence:
- **Hourly** for `5min` (most active tier).
- **Daily** for `30min` and `4hour` β cheap and keeps the long-range archive
current.
Pure-cron example (run hourly, only the active tier; do a full sweep nightly):
```cron
0 * * * * cd ~/repos/time-series-ai-weather-forecast && .venv/bin/python -m src.sync --cycle 5min >> data/sync.log 2>&1
30 3 * * * cd ~/repos/time-series-ai-weather-forecast && .venv/bin/python -m src.sync >> data/sync.log 2>&1
```
Inspect the archive directly with sqlite:
```bash
sqlite3 data/ecowitt.db "SELECT cycle_type, COUNT(*), MIN(datetime(ts_unix,'unixepoch')), MAX(datetime(ts_unix,'unixepoch')) FROM readings GROUP BY cycle_type"
sqlite3 data/ecowitt.db "SELECT * FROM fetch_log ORDER BY id DESC LIMIT 10"
```
If the run aborts on a rate-limit (`code=-1`), the script stops cleanly after
backing off twice β just re-run later and it picks up from where it left off.
## Quirks
- **Strings, not numbers.** Timestamps and values both arrive as strings.
- **Sparse data is possible.** Different metrics may have slightly different
sets of timestamps (sensors drop in and out).
- **Per-call size.** For `cycle_type=30min` over 7 days with a few channels,
responses are ~50 KB. `5min` for the same window is ~10Γ larger. Chunk
multi-month ranges to be safe.
- **Rate limit (observed).** The API returns `code=-1`,
`msg="The number of interface accesses reached the upper limit"` after
~30 calls in quick succession. The exact threshold and reset window aren't
documented; in practice a 60β120s sleep clears it. Sync handles this with
exponential backoff (60s β 120s) and stops gracefully if still throttled
so a re-run can resume from where it left off.
- **Channel availability is station-specific.** GW3000B exposes `outdoor`,
`indoor`, `solar_and_uvi`, `rainfall_piezo`, `wind`, `pressure`, `battery`.
Other stations differ.
- **Units may change.** Returned in whatever unit the device is configured for
(mine is imperial). Persist `unit` alongside the value so we don't lose it.
|