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, 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
After publication:
pip install xiaomi-weather
Until then, install this checkout with pip install . or a locally built wheel.
On 2026-10-05, https://pypi.org/pypi/xiaomi-weather/json returned HTTP 404:
there was no visible existing project, so xiaomi-weather was selected and the
fallback pyxiaomiweather was unnecessary. Availability is not a reservation;
PyPI may reserve names and the result must be rechecked before first publication.
Nothing has been registered or uploaded by this project setup.
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())
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.get_weather(city_id, latitude, longitude) -> WeatherDataparse_weather(payload: JSONValue) -> WeatherDataandparse_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.JSONValuedescribes 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.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| xiaomi_weather-0.1.0.tar.gz | 172.9 kB | With provenance | Details for xiaomi_weather-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| xiaomi_weather-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for xiaomi_weather-0.1.0-py3-none-any.whl |
Total release size: 192.1 kB
Release files / xiaomi_weather-0.1.0.tar.gz
| Download URL | xiaomi_weather-0.1.0.tar.gz |
|---|---|
| Size | 172.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2736287dd087b1f874d97329789975c545afac354b0999634c8747a3bc24ba74
|
|
BLAKE2b-256 checksum How to use checksums |
be210f6a8d252c54ca21aca682cbe2838a24cffd3c3e83a8695cdddd6795a024
|
| 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 4, 2026.
Transparency logRelease files / xiaomi_weather-0.1.0-py3-none-any.whl
| Download URL | xiaomi_weather-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ec9cfc8f2b01acf53793a5a324e500279d43d15062f96cb0679c46d981cb0c5b
|
|
BLAKE2b-256 checksum How to use checksums |
0fdb47027ab7fc3cb606ac5214adac7d68fd3c48c5d4a4cac0146276385c0a63
|
| 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 4, 2026.
Transparency logSecurity for xiaomi-weather 0.1.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.
Attestation for xiaomi_weather-0.1.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 4, 2026.
Transparency logA 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.