Skip to main content

pytp357s

Fetch and visualize history from ThermoPro TP357S Bluetooth Low Energy temperature/humidity sensors, without the official app.

The TP357S uses an undocumented, reverse-engineered protocol (different from the original TP357) -- see PROTOCOL.md for the technical details, open questions, and known limitations.

Features

  • Fetch full history or live readings over BLE
  • Incremental fetching with overlap verification and gap detection
  • SQLite storage, ready to use as a Grafana data source
  • Built-in plotting (interactive or PNG) as a lightweight alternative to Grafana
  • Multi-device, with configurable concurrency

Installation

pip install pytp357s

Or from source:

git clone https://github.com/giovannipizzi/pytp357s.git
cd pytp357s
pip install .

Requires Python 3.9+. Depends on bleak (BLE), PyYAML, and matplotlib.

Configuration

All commands need a config file describing your devices. Copy the example and edit it:

cp examples/devices.example.yaml devices.yaml
defaults:
  max_fetch_count: 20000   # max records to request when no prior data exists
  overlap: 15
  timeout: 120
  scan_timeout: 20        # BLE discovery timeout (macOS needs ~20s)
  parallel: 2

devices:
  S1:
    mac: "AA:BB:CC:11:22:33"
    name: "Sensor Kitchen"
    room: "Kitchen"
  S2:
    mac: "AA:BB:CC:44:55:66"
    name: "Sensor Bedroom"
    room: "Bedroom"

Finding your devices' MAC addresses -- run a BLE scan:

python -c "from bleak import BleakScanner; import asyncio; \
           print(asyncio.run(BleakScanner.discover(timeout=20)))"

Look for entries named TP357S (XXXX). (The last 4 characters XXXX in brackets are typically the last 4 characters of the MAC address, e.g. 5566 if the MAC is AA:BB:CC:44:55:66).

Config file location

Commands look for the config file in this order:

  1. --config /path/to/devices.yaml (explicit, highest priority)
  2. ./devices.yaml (current working directory)
  3. ~/.config/pytp357s/devices.yaml

If none of these exist, the command exits with an error explaining all three options.

The defaults: section sets global defaults for --count, --overlap, --timeout, and --parallel; every value can still be overridden per command via CLI flags.

Usage

Fetch live readings

(Note: This only gets current reading, not the historical data, and uses a different protocol, but it's useful e.g. to check which device is in range).

pytp357s fetch-data S1 --live
pytp357s fetch-data all --live

Fetch full history (print to stdout, no storage)

pytp357s fetch-data S1

Fetch full history into a SQLite database

pytp357s fetch-data all --db sensors.db

If sensors.db already has data for a device, this is refused (to avoid accidental duplicate full fetches). Use instead --incremental or a different --db path.

Incremental updates (for cron/scheduled runs)

pytp357s fetch-data all --db sensors.db --incremental

This fetches only records newer than the last stored entry for each device (plus a small overlap, used to verify continuity), and:

  • refuses to write if the overlap doesn't match what's stored (possible clock issue: if you run the command from different computers, make sure your clocks are synchronized!) or if a gap is detected (e.g. if the device was out of range too long and data is not stored anymore in the device). Use --force to write anyway (a GAP marker row is inserted when forcing past a detected gap)
  • performs a full first fetch automatically if the device has no prior data in this database
# Force past a mismatch/gap (e.g. after the device was out of range for a while)
pytp357s fetch-data all --db sensors.db --incremental --force

# Debug an overlap mismatch in detail
pytp357s fetch-data S1 --db sensors.db --incremental --debug-overlap

# Run devices serially instead of 2-at-a-time (helps avoid BLE adapter contention)
pytp357s fetch-data all --db sensors.db --incremental --parallel 1

Update room names / device metadata only

fetch-data already syncs device metadata (mac, name, room) into the database on every run, so this is rarely needed -- but if you've just edited devices.yaml and want the database updated immediately without a BLE fetch, you can run:

pytp357s update-rooms --db sensors.db

Plot

# Interactive window
pytp357s plot --db sensors.db

# Save to PNG
pytp357s plot --db sensors.db --output plot.png

# Last 24 hours only
pytp357s plot --db sensors.db --hours 24

Using as a Python library

import asyncio
from pytp357s import protocol, storage
from pytp357s.config import load_config
from pytp357s.fetcher import process_devices

cfg = load_config("devices.yaml")
device = cfg["devices"]["S1"]

# Live reading
temp, hum = asyncio.run(protocol.ble_fetch_live(device["mac"]))
print(f"{temp:.1f} C, {hum}%")

# Full history for one or more devices (recommended high-level API)
results = asyncio.run(process_devices(
    devices={"S1": device},
    live=False,
    db_path=None,
    incremental=False,
    count=None,
    overlap=0,
    timeout=120,
    scan_timeout=20,
    parallelism=1,
    force=False,
    max_fetch_count=20000,
))
timestamped = results["S1"].data  # list of (datetime, temp, hum)

# Store it
storage.init_db("sensors.db")
storage.append("sensors.db", "S1", timestamped)

For the full fetch pipeline (incremental logic, overlap/gap verification), see pytp357s.fetcher.process_devices().

Scanning devices

This is useful to see how many devices are in range. Note that this can only see the instantaneous temperature, to download the historical data you have to use the functions described above.

import asyncio
from bleak import BleakScanner

async def main():
    # Note: 20 seconds might be needed e.g. on macOS
    # to see all devices
    print("Scanning for 20 seconds...")
    devices = await BleakScanner.discover(timeout=20)
    print(f"\nFound {len(devices)} device(s):\n")
    for d in devices:
        print(f"  {d.address}  {d.name!r}")

    tp357 = [d for d in devices if d.name and "TP357" in d.name]
    print(f"\nTP357 devices found: {len(tp357)}")
    for d in tp357:
        print(f"  {d.address}  {d.name!r}")

asyncio.run(main())

Protocol details

See PROTOCOL.md for the full reverse-engineered protocol specification, including open questions and known limitations.

Acknowledgements

This project builds on the work of others:

Inspiration:

  • tpy357 and pasky/tp357 -- prior TP357 implementations that were an invaluable starting point and reference while reverse-engineering the TP357S's different protocol
  • The BTSnoop format documentation on the Wireshark wiki, used while parsing Android HCI snoop logs during protocol analysis

Dependencies:

  • bleak -- the Bluetooth LE library this project is built on
  • PyYAML and matplotlib -- configuration and plotting

Thanks to all of the above for making this possible.

License

MIT

Metadata

Release files for pytp357s 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 pytp357s 0.2.0
File Size Uploaded Provenance
pytp357s-0.2.0.tar.gz 24.6 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 49.2 kB

Release files / pytp357s-0.2.0.tar.gz

Download URL pytp357s-0.2.0.tar.gz
Size 24.6 kB
Tags Source
SHA-256 checksum
How to use checksums
429464a0183f16eddac4a237a4fa17de7876a029ec05216d5f5ffa4875ca115c
BLAKE2b-256 checksum
How to use checksums
867b67c260df8c0b9772c13422d9678b731542876fecc7d44a3c73b40073c449
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.12

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

Download URL pytp357s-0.2.0-py3-none-any.whl
Size 24.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
267e53c98b893024b7311b1e59361ccabc4d666ae1f9edb869dc73b78a9c5403
BLAKE2b-256 checksum
How to use checksums
a24719a10d7beb2512d4faf244e51bf8380b3564228b6d290409371709d369cc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.12

Security for pytp357s 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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

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.

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