PyCoway is a typed asyncio client for Coway AIRMEGA devices, covering cloud authentication, purifier status, and remote control through Coway IoCare.
- Async API built on aiohttp
- Typed dataclass models for purifier state
- Device control: power, fan speed, light, timers, modes, button lock, and more
- Air-quality readings: PM2.5, PM10, CO2, VOC, AQI
- Filter health monitoring: pre-filter, MAX2, and odor filter with detailed supply info
- Unified data from three API sources: IoT JSON, legacy REST, and HTML scrape
- Automatic token and session management
- Full test coverage with GitHub Actions CI
- Automated semantic version bumping, GitHub releases, and PyPI publishing
- Python 3.11 or newer
- A Coway IoCare account with at least one registered purifier
pip install pycowayFor local development:
git clone https://github.com/Antonio112009/pycoway.git
cd pycoway
pip install -e ".[dev]"import asyncio
from pycoway import CowayClient
async def main() -> None:
async with CowayClient("email@example.com", "password") as client:
await client.login()
data = await client.async_get_purifiers_data()
for device_id, purifier in data.purifiers.items():
print(f"{purifier.device_attr.name} ({device_id})")
print(f" Power: {'On' if purifier.is_on else 'Off'}")
print(f" Fan Speed: {purifier.fan_speed}")
print(f" PM2.5: {purifier.particulate_matter_2_5}")
print(f" AQI: {purifier.air_quality_index}")
asyncio.run(main())Coway requires users to change their password every 60 days. If the password hasn't been updated within that window, the API returns a password-change form instead of completing login, causing a PasswordExpired exception.
To skip this prompt and continue logging in without changing your password, set skip_password_change to True before calling login():
client = CowayClient("email@example.com", "password", skip_password_change=True)
await client.login()Note: This does not disable the Coway password policy — it simply submits the "change next time" option on the password-change page so login can proceed.
Every control method accepts the device_attr from a CowayPurifier instance:
import asyncio
from pycoway import CowayClient, LightMode
async def control_first_purifier() -> None:
async with CowayClient("email@example.com", "password") as client:
await client.login()
data = await client.async_get_purifiers_data()
purifier = next(iter(data.purifiers.values()))
attr = purifier.device_attr
await client.async_set_power(attr, is_on=True)
await client.async_set_auto_mode(attr)
await client.async_set_fan_speed(attr, speed="2")
await client.async_set_light(attr, light_on=True)
await client.async_set_light_mode(attr, LightMode.AQI_OFF)
await client.async_set_timer(attr, time="120")
asyncio.run(control_first_purifier())| Method | Parameters | Description |
|---|---|---|
async_set_power() |
is_on: bool |
Turn purifier on or off |
async_set_auto_mode() |
— | Switch to auto mode |
async_set_night_mode() |
— | Switch to night mode |
async_set_eco_mode() |
— | Switch to eco mode (AP-1512HHS only) |
async_set_rapid_mode() |
— | Switch to rapid mode (250s only) |
async_set_fan_speed() |
speed: str |
Set fan speed: "1", "2", or "3" |
async_set_light() |
light_on: bool |
Toggle light on/off (not for 250s) |
async_set_light_mode() |
light_mode: LightMode |
Set light mode for advanced models |
async_set_timer() |
time: str |
Off timer in minutes: "0", "60", "120", "240", "480" |
async_set_smart_mode_sensitivity() |
sensitivity: str |
"1" sensitive, "2" moderate, "3" insensitive |
async_set_button_lock() |
value: str |
"1" lock, "0" unlock |
async_change_prefilter_setting() |
value: int |
Wash frequency: 2, 3, or 4 weeks |
async_get_purifiers_data() returns a PurifierData dataclass containing a purifiers dictionary keyed by device ID.
Each CowayPurifier includes:
| Field | Type | Description |
|---|---|---|
device_attr |
DeviceAttributes |
Device ID, model, name, place ID |
mcu_version |
str | None |
Firmware version |
network_status |
bool | None |
Network connectivity |
| Field | Type | Description |
|---|---|---|
is_on |
bool | None |
Power state |
auto_mode |
bool | None |
Auto mode |
eco_mode |
bool | None |
Eco mode |
night_mode |
bool | None |
Night mode |
rapid_mode |
bool | None |
Rapid mode |
fan_speed |
int | None |
Fan speed level |
light_on |
bool | None |
Light state |
light_mode |
int | None |
Device-specific light mode |
button_lock |
int | None |
Button lock state |
smart_mode_sensitivity |
int | None |
Smart mode sensitivity level |
timer |
str | None |
Configured off timer |
timer_remaining |
int | None |
Remaining timer (minutes) |
| Field | Type | Description |
|---|---|---|
particulate_matter_2_5 |
int | None |
PM2.5 (μg/m³) |
particulate_matter_10 |
int | None |
PM10 (μg/m³) |
carbon_dioxide |
int | None |
CO₂ (ppm) |
volatile_organic_compounds |
int | None |
VOC level |
air_quality_index |
int | None |
AQI value |
aq_grade |
int | None |
Air quality grade |
lux_sensor |
int | None |
Ambient light sensor |
| Field | Type | Description |
|---|---|---|
pre_filter_pct |
int | None |
Pre-filter remaining (%) |
pre_filter_change_frequency |
int | None |
Wash frequency (weeks) |
max2_pct |
int | None |
MAX2 filter remaining (%) |
odor_filter_pct |
int | None |
Odor filter remaining (%) |
filters |
list[FilterInfo] | None |
Detailed info for each filter/supply |
Each FilterInfo object in the filters list provides detailed supply data from the IoCare API:
| Field | Type | Description |
|---|---|---|
name |
str | None |
Filter name (e.g. "Pre-Filter", "Max2 Filter") |
filter_remain |
int | None |
Filter life remaining (%) |
filter_remain_status |
str | None |
Status: INITIAL, AVAILABLE, or REPLACE |
replace_cycle |
int | None |
Replacement cycle value |
replace_cycle_unit |
str | None |
Cycle unit: W (weeks) or M (months) |
last_date |
str | None |
Last filter change date |
next_date |
str | None |
Next recommended change date |
pollutants |
list[str] |
Pollutants the filter targets (e.g. "Pollen", "VOCs") |
description |
str | None |
What the filter removes |
pre_filter |
bool |
Whether this is a pre-filter |
server_reset |
bool |
Whether the filter can be reset remotely |
For the complete schema, see src/pycoway/devices/models.py.
async_get_purifiers_data() combines three API sources automatically:
| Source | Base URL | Provides |
|---|---|---|
| IoT JSON API | iocareapi.iot.coway.com |
Device status, sensors, timer, air quality |
| Legacy REST API | iocare2.coway.com/api/proxy |
Rich filter data (descriptions, pollutants, dates) |
| HTML Scrape | iocare2.coway.com/en |
MCU firmware version, lux sensor |
All exceptions inherit from CowayError:
from pycoway import AuthError, CowayError, PasswordExpired| Exception | Description |
|---|---|
CowayError |
Base exception for all library errors |
AuthError |
Authentication failed |
PasswordExpired |
Coway requires a password change |
ServerMaintenance |
Coway API is under maintenance |
RateLimited |
Coway temporarily blocked the account |
NoPlaces |
No places configured in the IoCare account |
NoPurifiers |
No air purifiers found |
If you're switching from the original cowayaio package:
pip uninstall cowayaio
pip install pycowayUpdate your imports:
# Before
from cowayaio import CowayClient
# After
from pycoway import CowayClientgit clone https://github.com/Antonio112009/pycoway.git
cd pycoway
pip install -e ".[dev]"
pytest
ruff check .
ruff format --check .Feature work should branch from development, and pull requests merge into development first. See CONTRIBUTING.md for the full workflow.
- PRs from
developmenttomaintrigger the release workflow when merged - The workflow bumps
src/pycoway/__version__.py - PRs to
mainmust have exactly one version label:patch,minor, ormajor - A git tag and GitHub release are created automatically
- The package is published to PyPI automatically
src/pycoway/
├── __init__.py # Public API exports
├── __version__.py # Version string
├── client.py # Public CowayClient entry point
├── constants.py # API constants, sensor codes, IAQ field mapping
├── exceptions.py # Public exception hierarchy
├── py.typed # PEP 561 marker
├── account/
│ ├── auth.py # Authentication (login, token refresh)
│ └── maintenance.py # Server maintenance checks
├── devices/
│ ├── control.py # Purifier control commands
│ ├── data.py # Data fetching (legacy HTML + IoT JSON API)
│ ├── models.py # Dataclasses (CowayPurifier, FilterInfo, PurifierData)
│ └── parser.py # HTML/JSON response parsing and normalisation
└── transport/
└── http.py # HTTP base client with session management
MIT, originally authored by RobertD502