Typed asyncio client for Coway AIRMEGA devices through IoCare
Project description
PyCoway
PyCoway is a typed asyncio client for Coway AIRMEGA devices, covering cloud authentication, purifier status, and remote control through Coway IoCare.
Features
- 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
Requirements
- Python 3.11 or newer
- A Coway IoCare account with at least one registered purifier
Installation
pip install pycoway
For local development:
git clone https://github.com/Antonio112009/pycoway.git
cd pycoway
pip install -e ".[dev]"
Quick Start
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())
Skipping Password Change Prompt
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.
Device Control
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())
Available Control Methods
| 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 |
Data Model
async_get_purifiers_data() returns a PurifierData dataclass containing a purifiers dictionary keyed by device ID.
Each CowayPurifier includes:
Device Identity
| Field | Type | Description |
|---|---|---|
device_attr |
DeviceAttributes |
Device ID, model, name, place ID |
mcu_version |
str | None |
Firmware version |
network_status |
bool | None |
Network connectivity |
Control State
| 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) |
Air Quality
| 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 |
Filter Health
| 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 |
FilterInfo
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.
Data Sources
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 |
Exceptions
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 |
Migrating from cowayaio
If you're switching from the original cowayaio package:
pip uninstall cowayaio
pip install pycoway
Update your imports:
# Before
from cowayaio import CowayClient
# After
from pycoway import CowayClient
Development
git 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.
Release Flow
- 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
Project Structure
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
License
MIT, originally authored by RobertD502
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pycoway-2.0.2.tar.gz.
File metadata
- Download URL: pycoway-2.0.2.tar.gz
- Upload date:
- Size: 34.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f3afde81fed3b01b46c34c234986de37ba79b4b47de8189c31dc0aef3869023d
|
|
| MD5 |
b983a7d8964dc54fbbe964dbf80464a6
|
|
| BLAKE2b-256 |
3b37fafb5da8edde996f60411f5128567adfb2024ebf13f1a412859d46b415f6
|
Provenance
The following attestation bundles were made for pycoway-2.0.2.tar.gz:
Publisher:
release.yml on Antonio112009/pycoway
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycoway-2.0.2.tar.gz -
Subject digest:
f3afde81fed3b01b46c34c234986de37ba79b4b47de8189c31dc0aef3869023d - Sigstore transparency entry: 1383933105
- Sigstore integration time:
-
Permalink:
Antonio112009/pycoway@94e35a05b9174cbb2019936f2cf6f8175f8f66f8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Antonio112009
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@94e35a05b9174cbb2019936f2cf6f8175f8f66f8 -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file pycoway-2.0.2-py3-none-any.whl.
File metadata
- Download URL: pycoway-2.0.2-py3-none-any.whl
- Upload date:
- Size: 28.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
150d3e593c332be9992a03cfc2f7cf266573f479df2f91647874a00545f2dabe
|
|
| MD5 |
9fb21df2d0d6f3b4845a0950c89abfdd
|
|
| BLAKE2b-256 |
949e975a59bb048b8d4940891cf0198cf59a12948f955323b83d7da9ea4650cd
|
Provenance
The following attestation bundles were made for pycoway-2.0.2-py3-none-any.whl:
Publisher:
release.yml on Antonio112009/pycoway
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycoway-2.0.2-py3-none-any.whl -
Subject digest:
150d3e593c332be9992a03cfc2f7cf266573f479df2f91647874a00545f2dabe - Sigstore transparency entry: 1383933127
- Sigstore integration time:
-
Permalink:
Antonio112009/pycoway@94e35a05b9174cbb2019936f2cf6f8175f8f66f8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Antonio112009
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@94e35a05b9174cbb2019936f2cf6f8175f8f66f8 -
Trigger Event:
pull_request
-
Statement type: