Skip to main content

PyCoway

CI PyPI Python License: MIT Version

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.14 or newer, the version the current Home Assistant release runs on
  • 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 int | 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, including network failures: aiohttp connection errors, timeouts and truncated payloads are wrapped in CowayConnectionError, so a single except CowayError covers everything the client can raise.

from pycoway import AuthError, CowayConnectionError, CowayError, PasswordExpired
Exception Description
CowayError Base exception for all library errors
CowayConnectionError Network error talking to Coway (connection, timeout, payload); the aiohttp error is __cause__
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 development to main trigger the release workflow when merged
  • The workflow bumps src/pycoway/__version__.py
  • PRs to main must have exactly one version label: patch, minor, or major
  • 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

Release files for pycoway 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pycoway 3.0.0
File Size Uploaded
pycoway-3.0.0.tar.gz 44.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pycoway 3.0.0
File Interpreter ABI Platform
pycoway-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 77.3 kB

Release files / pycoway-3.0.0.tar.gz

Download URL pycoway-3.0.0.tar.gz
Size 44.7 kB
Tags Source
SHA-256 checksum
How to use checksums
489bebf488327148c3889d375bb94a99d85bf3b75b4f38c6415d0b0e43a80052
BLAKE2b-256 checksum
How to use checksums
120b01091517d33adfb662ec99a1b7489ed75aea411d6a8e33526b58105fd509
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / pycoway-3.0.0-py3-none-any.whl

Download URL pycoway-3.0.0-py3-none-any.whl
Size 32.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a9a71db53e273b2005904ec54ff3f780b450f0afbf552df7c36f2776dd529f89
BLAKE2b-256 checksum
How to use checksums
8d9875385193aeb9cca6b6c1d57dc7aeed7d5f4f0d27453c2a31784d58ca2b15
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

3.0.0 This release

2 release files

2.0.6

2 release files

2.0.5

2 release files

2.0.4

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.6.1

2 release files

1.6.0

2 release files

1.5.1

2 release files

1.5.0

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page