multi-user-scale-core
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.
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. Callto_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.
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)
| File | Size | Uploaded | |
|---|---|---|---|
| multi_user_scale_core-0.2.1.tar.gz | 20.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|