Skip to main content

multi-user-scale-core

PyPI version

Not every smart scale app or integration surfaces who's standing on the scale. This library solves that: given a weight reading and a set of users with history, it returns a ranked list of likely owners. Pure Python, no runtime dependencies. Fully typed (PEP 561).

Features

  • WeightRouter: Route incoming weight measurements to users using adaptive tolerance and competitive pruning.
  • Adaptive tolerance: Exponentially-weighted reference weight, variance-based tolerance, and recency scaling that automatically widens the window when a user hasn't weighed in recently.
  • Dormant User Retention: Users inactive past the retention window retain their baseline for future matching with an expanded tolerance margin.
  • Persistence: to_dict() / from_dict() for saving and restoring router state across restarts.
  • Models: WeightMeasurement, UserProfile, RouterConfig, MeasurementCandidate.

Buy Me A Coffee

Installation

Requires Python 3.10+. Install using pip:

pip install multi-user-scale-core

Quick Start

from multi_user_scale_core import RouterConfig, UserProfile, WeightMeasurement, WeightRouter
from datetime import datetime, timezone

router = WeightRouter(config=RouterConfig())
router.set_users([
    UserProfile(user_id="alice", display_name="Alice"),
    UserProfile(user_id="bob", display_name="Bob"),
])

# Evaluate an incoming measurement (e.g. from a scale sensor)
measurement = WeightMeasurement(
    weight_kg=75.2,
    timestamp=datetime.now(tz=timezone.utc),
    source_id="sensor.scale",
)
candidates = router.evaluate_measurement(measurement)
# candidates: list[MeasurementCandidate], ordered by match quality.
# Matched users come first, sorted by proximity to their reference weight.
# Users with no history yet are appended at the end.
#
# Matched candidates include:
#   .reference_weight_kg  — the weighted-average reference used for comparison
#   .tolerance_kg         — the tolerance band that accepted this reading
# No-history candidates have both fields as None.

# Once confirmed (e.g. by the user), record the measurement
router.record_measurement_for_user("alice", measurement)

Usage

Reassigning and removing measurements

# Move the latest measurement from alice to bob (e.g. after user correction)
router.reassign_measurement("alice", "bob")

# Move a specific measurement by ID
router.reassign_measurement("alice", "bob", measurement_id="abc123")

# Remove the latest measurement for a user
router.remove_measurement("alice")

# Remove a specific measurement by ID
router.remove_measurement("alice", measurement_id="abc123")

Managing users

router.set_users([
    UserProfile(user_id="alice", display_name="Alice"),
    UserProfile(user_id="bob", display_name="Bob"),
])

Note: set_users() replaces the entire user list. History for any user not present in the new list is permanently discarded. Call to_dict() first if you need to preserve that history.

Persistence

# Serialise state (e.g. to Home Assistant config entry data)
payload = router.to_dict()

# Restore state
router = WeightRouter.from_dict(payload)

# Inject a custom clock (useful in tests or when the stored "now" matters
# for pruning stale history on first mutation after restore)
router = WeightRouter.from_dict(payload, now_provider=lambda: my_fixed_time)

to_dict() includes a "now" snapshot timestamp for human inspection. It is not used during from_dict() restoration.

from_dict() restores history leniently: an individual measurement that cannot be restored — malformed entry, missing or non-finite weight_kg, missing or unparseable timestamp, duplicate measurement_id (the first occurrence wins), or a history block for a user that is not configured — is skipped with a warning on the multi_user_scale_core.router logger rather than failing the whole restore. Structural problems still raise: a payload, config, users or history container of the wrong type.

Configuration

config = RouterConfig(
    history_retention_days=90,       # drop measurements older than this (retaining the most recent 1)
    max_history_size=100,            # cap per-user history length
    tolerance_percentage=0.04,       # base tolerance as fraction of body weight
    min_tolerance_kg=1.5,            # floor on tolerance regardless of body weight
    variance_window_days=30,         # window for variance-based adaptive tolerance
    reference_window_days=7,         # window for computing the reference weight
    min_measurements_for_adaptive=5, # minimum history needed for variance adaptation
    enable_pruning=True,             # drop candidates significantly worse than the best match
    prune_margin_kg=3.0,             # margin above the best match to start pruning
    keep_history_forever=False,      # when True, never prune history (retention/size ignored)
)

Default tolerance constants are exported for convenience:

from multi_user_scale_core import (
    DEFAULT_TOLERANCE_PERCENTAGE,  # 0.04
    MIN_TOLERANCE_KG,              # 1.5
    MAX_TOLERANCE_KG,              # 5.0
    MIN_MEASUREMENTS_FOR_ADAPTIVE, # 5
    REFERENCE_WINDOW_DAYS,         # 7
    VARIANCE_WINDOW_DAYS,          # 30
)

Error handling

All errors inherit from RouterError:

from multi_user_scale_core import (
    DuplicateMeasurementError,    # measurement_id already exists in history
    MeasurementNotFoundError,     # referenced measurement does not exist
    MeasurementValidationError,   # weight is NaN, infinite, or not a number
    RouterError,                  # base class
    UserNotFoundError,            # user_id not registered with set_users()
)

Compatibility

  • Python 3.10+
  • No runtime dependencies

Support the Project

If you find this project helpful, consider buying me a coffee! Your support helps maintain and improve this library.

Buy Me A Coffee

License

This project is licensed under the MIT License - see the LICENSE file for details.

Release files for multi-user-scale-core 0.2.1

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

Source distribution (sdist)

Source distribution for multi-user-scale-core 0.2.1
File Size Uploaded
multi_user_scale_core-0.2.1.tar.gz 20.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for multi-user-scale-core 0.2.1
File Interpreter ABI Platform
multi_user_scale_core-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 34.1 kB

Release files / multi_user_scale_core-0.2.1.tar.gz

Download URL multi_user_scale_core-0.2.1.tar.gz
Size 20.9 kB
Tags Source
SHA-256 checksum
How to use checksums
5f217a5d15c321edb8c96744497aa18197a7dc565ed75915b98cb58f9c0710be
BLAKE2b-256 checksum
How to use checksums
6057e7e4e2a9304a4e808458f48f010ab0eb01675b8792d2efcbc81063c92de6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / multi_user_scale_core-0.2.1-py3-none-any.whl

Download URL multi_user_scale_core-0.2.1-py3-none-any.whl
Size 13.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
37055dd77b30cbe83b6416c8e8b75494c1e57f78e9f667b24d9b33ddc9990987
BLAKE2b-256 checksum
How to use checksums
e68e7f95971483cfaffa7fdf9ffd9e76758f1b8e24950dcd6396eb0c33fde548
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.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