Metadata-Version: 2.4 Name: pyfreshr Version: 1.2.0 Summary: Async Python client for the Fresh-r / bw-log.com API. Author: Leon Grave License: MIT Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Framework :: AsyncIO Classifier: Intended Audience :: Developers Requires-Python: >=3.13 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: aiohttp>=3.8 Provides-Extra: dev Requires-Dist: pytest>=7.0; extra == "dev" Requires-Dist: pytest-asyncio>=0.22; extra == "dev" Requires-Dist: pytest-cov>=5.0; extra == "dev" Requires-Dist: ruff>=0.4; extra == "dev" Requires-Dist: mypy>=1.0; extra == "dev" Dynamic: license-file # pyfreshr [![Build](https://github.com/SierraNL/pyfreshr/actions/workflows/build.yml/badge.svg)](https://github.com/SierraNL/pyfreshr/actions/workflows/build.yml) [![PyPI version](https://img.shields.io/pypi/v/pyfreshr)](https://pypi.org/project/pyfreshr/) [![Python versions](https://img.shields.io/pypi/pyversions/pyfreshr)](https://pypi.org/project/pyfreshr/) [![License: MIT](https://img.shields.io/github/license/SierraNL/pyfreshr)](https://github.com/SierraNL/pyfreshr/blob/main/LICENSE) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) Python library around the Fresh-r web interface for use in a Home Assistant integration. ## Installation ```bash pip install pyfreshr ``` ## Quick start ```python import asyncio from pyfreshr import FreshrClient async def main(): async with FreshrClient() as client: await client.login("user@example.com", "password") devices = await client.fetch_devices() for device in devices: readings = await client.fetch_device_current(device) print(device.id, readings.t1, readings.flow, readings.co2) asyncio.run(main()) ``` ## Session persistence The login sequence performs three HTTP round-trips. To avoid repeating this on every Home Assistant restart, save the session token after login and restore it on the next startup. The client re-authenticates automatically if the restored token has expired. ```python from pyfreshr import FreshrClient async def setup(hass, stored_token): client = FreshrClient(on_session_update=lambda token: hass.store.save(token)) if stored_token: client.restore_session(stored_token) # skip login if token is still valid else: await client.login("user@example.com", "password") return client ``` ## Logging The library uses Python's standard `logging` module under the logger name `pyfreshr.client`. To see debug output (HTTP requests, response statuses, session events) enable it in your application: ```python import logging logging.getLogger("pyfreshr.client").setLevel(logging.DEBUG) ``` In Home Assistant, add this to your `configuration.yaml`: ```yaml logger: logs: pyfreshr.client: debug ``` ## Example script Run the example script (without installing the package) from the repository root: ```bash PYTHONPATH=src python examples/example_usage.py ``` You can also set credentials via environment variables `FRESHR_USER` and `FRESHR_PASS` before running the script. ## Models ### `DeviceSummary` Returned by `fetch_devices()`. Contains the device identifier and metadata. | Field | Type | Description | |---|---|---| | `id` | `str \| None` | Device serial number | | `type` | `str` | Raw type string from the API (e.g. `"fresh-r-itw"`) | | `active_from` | `str \| None` | Activation date | | `device_type` | `DeviceType` | Categorised type derived from `type` (property) | | `extras` | `dict` | Any additional fields returned by the API | ### `DeviceReadings` Returned by `fetch_device_current()`. All numeric fields are `None` when the API does not return a value for the device type. See [Value processing](#value-processing) for fields that are calibrated before being returned. | Field | Type | Unit | Description | |---|---|---|---| | `t1` | `float \| None` | °C | Supply air temperature | | `t2` | `float \| None` | °C | Extract air temperature | | `t3` | `float \| None` | °C | Temperature sensor 3 | | `t4` | `float \| None` | °C | Temperature sensor 4 | | `flow` | `float \| None` | m³/h | Ventilation flow (calibrated) | | `co2` | `int \| None` | ppm | CO₂ concentration | | `hum` | `float \| None` | %RH | Relative humidity (temperature-adjusted) | | `dp` | `float \| None` | °C | Dew point | | `temp` | `float \| None` | °C | Temperature sensor (Forward and Monitor only) | | `extras` | `dict` | — | All other fields from the API, including particle measurements (`d5_25`, `d1_25`, etc. in µg/m³) | ## Supported devices The dashboard exposes four device types. Three are supported; the fourth (Extract) uses a separate external API that is not covered by this library. | Device type | `DeviceType` | Supported | |---|---|---| | Fresh-R | `DeviceType.FRESH_R` | Yes | | Forward | `DeviceType.FORWARD` | Yes (untested) | | Monitor | `DeviceType.MONITOR` | Yes (untested) | | Fresh-R Extract | — | No | The device type is detected automatically from the `type` string returned by the API (substring match, mirroring the dashboard JS). `fetch_device_current` accepts a `DeviceSummary` directly and uses its type to select the correct API request name and default field list. ## Value processing Raw API values are calibrated before being returned, matching the processing performed by the dashboard JavaScript (`processCurrentData`). The processed values are reflected in the `DeviceReadings` fields. ### Flow (`DeviceReadings.flow`) Flow is calibrated through a piecewise curve. For **Forward** devices the raw sensor value is first divided by 3 before the curve is applied. ``` if raw_flow > 200: flow = (raw_flow − 700) / 30 + 20 else: flow = raw_flow flow = round(flow, 1) # m³/h ``` ### Humidity (`DeviceReadings.hum`) Humidity is adjusted for the supply-air temperature using the Magnus-Tetens formula. The reference temperature used depends on device type: | Device type | Reference temperature | |---|---| | Fresh-R | `t1` (supply air temperature) | | Forward | `temp` (temperature sensor) | | Monitor | None — raw humidity is rounded to 1 dp | ``` T_sh = 243.04 × (a − ln(hum/100)) / (17.625 + ln(hum/100) − a) where a = 17.625 × dp / (243.04 + dp) hum_adj = hum × exp(4283.78 × (T_sh − T_ref) / (243.12 + T_sh) / (243.12 + T_ref)) hum_adj = round(hum_adj, 1) # %RH ``` If any input value is missing or the formula produces NaN the raw humidity (rounded to 1 dp) is returned instead.