Skip to main content

xiaomi-weather

A small, typed asyncio/aiohttp client for the undocumented API used by Xiaomi Weather. Independent of Home Assistant. Supports mainland-China weathercn city lookup by coordinates, name or ID, current conditions, daily/hourly forecasts and the observed AQI fields. No caching, retries, polling or synchronous client. Not affiliated with Xiaomi. The service may change or stop working without notice.

Requires Python 3.11+, including 3.14. The minimum follows use of asyncio.timeout; no Python 3.14-only syntax is required. The sole direct runtime dependency is aiohttp>=3.13.5,<4 (3.13 added Python 3.14 support).

Installation

pip install xiaomi-weather

Standalone use

import asyncio

from xiaomi_weather import XiaomiWeatherClient, XiaomiWeatherError


async def main() -> None:
    try:
        async with XiaomiWeatherClient() as client:
            locations = await client.locate(39.9, 116.4)
            if not locations:
                print("No supported weathercn city found")
                return
            # Applications should offer a choice when there are multiple matches.
            city = locations[0]
            weather = await client.get_weather(city.city_id, 39.9, 116.4)
            print(city.name, weather.current.temperature)
            print(weather.current.weather_code)  # Xiaomi code, e.g. "1"
            daily = weather.forecast_daily
            if daily is not None and daily.temperature is not None:
                print(daily.temperature.unit, daily.temperature.value)
    except XiaomiWeatherError as err:
        print(f"Weather unavailable: {err}")


asyncio.run(main())

Search and ID lookup return the same Location model as coordinate lookup. No match is an empty tuple; unsupported providers and failed entries are omitted. Coordinates remain optional provider metadata: callers must check them before using a search result to request weather. Names and IDs use the Chinese locale.

Supply the lookup coordinates with each weather request. city_id is a bare nine-digit identifier starting with 101, e.g. 101010100. Geographic input must be finite, latitude in [-90, 90], longitude in [-180, 180]. The client uses the reference application's zh_cn, isGlobal=false, 15-day request parameters; actual forecast length is controlled by the provider.

Inject a session

import asyncio
import aiohttp
from xiaomi_weather import XiaomiWeatherClient


async def main() -> None:
    async with aiohttp.ClientSession() as session:
        async with XiaomiWeatherClient(session, timeout=20) as client:
            weather = await client.get_weather("101010100", 39.9, 116.4)
            print(weather.current.temperature.value)
        assert not session.closed  # only the outer context owns this session


asyncio.run(main())

XiaomiWeatherClient(session=None, *, timeout=20) creates its own session lazily inside the running event loop. Use async with or await client.close(). close() is idempotent and never closes an injected session. A closed client cannot be reused, even with an external session. Keep a client on one event loop; finish/cancel its outstanding requests before closing it. Constructor does no I/O. The request timeout bounds the entire network operation; each request happens once.

Public API and data

  • await client.locate(latitude, longitude) -> tuple[Location, ...]
  • await client.search(name) -> tuple[Location, ...] searches a stripped, nonempty city name.
  • await client.get_city(city_id) -> tuple[Location, ...] resolves a bare nine-digit weathercn ID and returns only exact matches.
  • await client.get_weather(city_id, latitude, longitude) -> WeatherData
  • parse_weather(payload: JSONValue) -> WeatherData and parse_locations(payload: JSONValue) -> tuple[Location, ...] for offline parsing.
  • Frozen dataclasses exported at package root: Measurement, Pair, Series, Wind, CurrentWeather, DailyWind, DailyForecast, HourlyWind, HourlyForecast, WeatherData, Location, DailyForecastEntry, HourlyForecastEntry, AirQuality. JSONValue describes diagnostic raw JSON.

Python attributes use snake_case (pubTime → pub_time, feelsLike → feels_like, forecastDaily → forecast_daily, sunRiseSet → sun_rise_set). Pair.from_ and Pair.to retain the provider's from/to values. Series.value is a tuple whose None entries preserve original indices, with independent unit and pub_time fields. Arrays are never truncated to a common length.

Field Representation / observed unit
current.temperature, current.feels_like Measurement(value, unit), ℃
current.humidity Measurement, %
current.pressure Measurement, hPa
current.visibility Measurement, km
current.wind.speed, .direction Measurement, km/h and °
current.uv_index finite float or None; dimensionless
current.weather_code, hourly weather_code Xiaomi numeric code as a string; unknown codes retained (original weather fields also supported)
current.pub_time, series pub_time, solar times aware datetime, original UTC offset retained
forecast_daily.temperature Series[Pair[float]], observed from=high, to=low, ℃
forecast_daily.weather Series[Pair[str]], observed from=day, to=night
forecast_daily.sun_rise_set Series[Pair[datetime]], from=sunrise, to=sunset
forecast_daily.wind.speed, .direction Series[Pair[float]], from=day, to=night; km/h, °
forecast_daily.precipitation_probability Series[float], observed percentages 0–100, no rounding or fractional scaling
forecast_hourly.temperature, .weather separate Series; each has its own hourly start pub_time
forecast_hourly.wind Series[HourlyWind]; each entry has datetime, speed, direction
Location location_key, city_id property, name, affiliation, latitude, longitude, raw
air_quality AirQuality with aqi, pm25, pm10: finite floats or None; source does not declare AQI scale or concentration units

The legacy hourly wind fixture omits a unit: the typed series keeps unit=None. The reference weathercn integration interprets speed as km/h and direction as degrees. Applications may apply that provider-specific policy explicitly; the library never invents units. Unknown explicit units are preserved, never silently relabeled. No Celsius/Fahrenheit or UTC conversion takes place.

Required current temperature needs a finite value and a nonempty explicit unit; current pubTime must carry a timezone. Missing/failed optional blocks become None; malformed array elements remain None at their original positions. Booleans, blank/non-numeric values, NaN, infinity and -999 are unavailable numbers; zero is valid. Failed status blocks are isolated. Probabilities outside 0–100 are unavailable. Invalid or naive optional dates become None, while other values in the same series remain accessible.

WeatherData.raw and Location.raw retain the complete received JSON for diagnostics and experimentation, including status codes and unmodeled fields. Raw retention does not imply semantic support: alerts and moon phase, for example, are not currently modeled. Use weather.air_quality for the supported AQI fields; normal use of supported capabilities should not require raw JSON parsing. Raw dictionaries are mutable, excluded from dataclass equality, and are not synchronized with parsed fields. Offline parsers retain the passed dictionary by reference; treat it as caller-owned data. Typed forecast data is immutable.

Daily and twice-daily HA forecast objects are application projections, not separate upstream endpoints. Build them from daily pairs and solar times; use iter_daily() and iter_hourly() for provider-level alignment. See HA migration.

Aligned forecasts without another request

Use the result's synchronous iterators to avoid joining provider arrays yourself:

from xiaomi_weather import WeatherData


def show_forecasts(weather: WeatherData) -> None:
    for day in weather.iter_daily():
        print(
            day.index, day.temperature_high, day.temperature_low, day.temperature_unit
        )
        print(day.day_weather_code, day.night_weather_code, day.sunrise, day.sunset)

    for hour in weather.iter_hourly():
        print(
            hour.time,
            hour.temperature,
            hour.temperature_unit,
            hour.weather_code,
            hour.wind_speed,
            hour.wind_speed_unit,
        )

Both methods return fresh iterators over frozen dataclasses. Missing forecast blocks yield no entries. There is no I/O, mutation, cache, unit conversion, condition mapping or filtering of entries with missing temperatures. The original forecast_daily, forecast_hourly and raw remain available.

iter_daily() -> Iterator[DailyForecastEntry] aligns by original array index, using the longest of temperature, weather, solar, probability and both wind series. An entirely empty position still has an entry; shorter arrays contribute None. Entries expose index, temperature, temperature_unit, weather, sun_rise_set, precipitation_probability, precipitation_probability_unit, wind_speed, wind_speed_unit, wind_direction, wind_direction_unit. Use semantic properties temperature_high, temperature_low, day_weather_code, night_weather_code, sunrise, sunset without decoding from_/to yourself. Original pairs remain supported: a missing from_ never removes a valid to. No midnight, calendar date or daytime flag is invented.

iter_hourly() -> Iterator[HourlyForecastEntry] computes each temperature and weather sequence's times from its own aware pub_time plus its original hour index. Wind uses each entry's explicit datetime. It emits the chronological union of these timelines, including temperature gaps and weather/wind-only instants. Entries expose time, temperature, temperature_unit, weather, wind_speed, wind_speed_unit, wind_direction. Both views retain unknown codes, zeros and explicit unfamiliar units; daily probabilities stay unrounded.

Equal instants match even when timezone offsets differ. Different starting times are aligned at their actual overlapping instants, not rejected wholesale or joined by index. No rounding to an hour is performed. Duplicate wind timestamps use the last entry, including its missing fields. The output time keeps the first available source offset in temperature, weather, wind order. A series unit labels that source's field even if a particular row has no value. Legacy hourly wind still has no explicit unit; no direction unit is invented.

Hourly values without an aware time, or beyond datetime's representable range, cannot be placed on a timeline: they remain accessible in the original series and raw. They are not assigned another series' timestamp. These views do not replace the lossless source structures and do not turn missing data into zero.

Stability and supported scope

The public API contract defines supported fields, normalization, absence, equality and version compatibility. The library adapts observed upstream representation changes behind this interface; applications should not need a new raw parser each time Xiaomi changes a supported field. Semantic property names and existing public models are both supported.

raw itself follows Xiaomi's response and has no field-layout compatibility promise. Unmodeled data requires implementation and tests before it is a supported feature. The observed AQI fields are now modeled:

from xiaomi_weather import WeatherData


def show_air_quality(weather: WeatherData) -> None:
    quality = weather.air_quality
    if quality is not None:
        print(quality.aqi, quality.pm25, quality.pm10)

Missing AQI blocks/fields are optional. No pollutant unit or AQI standard is inferred from bare numbers. HA display conditions and user-selected unit conversion remain integration responsibilities.

Exceptions

All service failures derive from XiaomiWeatherError:

Exception Meaning
TransportError aiohttp connection/TLS/transfer failure
RequestTimeoutError timeout; subclass of TransportError
HTTPError unsuccessful HTTP response; integer .status (including 429)
InvalidResponseError invalid JSON/content type, response structure, failed or unusable required current data

Original transport/decoding exceptions are chained. HTTP response bodies and coordinate-bearing URLs are not included in error messages. ValueError signals invalid caller arguments, RuntimeError a closed client/session. asyncio.CancelledError is never caught or converted. Empty supported-location results are not errors.

Development and release

uv sync --locked
uv run pytest --cov=xiaomi_weather --cov-report=term-missing
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run python -m build
uv run twine check --strict dist/*
uv run python scripts/check_dist.py

Tests use local fixtures, mocked aiohttp boundaries and a loopback HTTP server; no live API calls are part of testing. Strict mypy checks both package and tests. CI runs Python 3.11–3.14 and verifies wheel/sdist installation and py.typed.

See release setup for manual GitHub/PyPI configuration and design decisions for official HA requirements and scope. License: Apache-2.0. See NOTICE and fixture provenance for Home Assistant attribution.

Metadata

Release files for xiaomi-weather 0.2.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 xiaomi-weather 0.2.0
File Size Uploaded Provenance Actions
xiaomi_weather-0.2.0.tar.gz 173.3 kB With provenance Details for xiaomi_weather-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for xiaomi-weather 0.2.0
File Interpreter ABI Platform Actions
xiaomi_weather-0.2.0-py3-none-any.whl Python 3 none any Details for xiaomi_weather-0.2.0-py3-none-any.whl

Total release size: 192.6 kB

Release files / xiaomi_weather-0.2.0.tar.gz

Download URL xiaomi_weather-0.2.0.tar.gz
Size 173.3 kB
Tags Source
SHA-256 checksum
How to use checksums
49ae94575529e7c35b453d61f487b901df1b8d9e8f6e594e3bb4a9419cfe0fa6
BLAKE2b-256 checksum
How to use checksums
59845d76b7386ac1d6d09cb9b0be0d6adfed0cae70a7a6db7374a1a8181225e6
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 Oct 19, 2026.

Transparency log

Release files / xiaomi_weather-0.2.0-py3-none-any.whl

Download URL xiaomi_weather-0.2.0-py3-none-any.whl
Size 19.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cfdbeb829c040b19f5ad6ca938cd3674fa4014c989148518e468a6362e9d5ba3
BLAKE2b-256 checksum
How to use checksums
ebd4efbdeb00860bb8e704276b7abf1c4ae1067b7ab3dc03e36af2dd2705132a
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 Oct 19, 2026.

Transparency log

Security for xiaomi-weather 0.2.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for xiaomi_weather-0.2.0.tar.gz

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 Oct 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

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