Core weight routing primitives for multi-user scale integrations
Project description
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.
- 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.
- 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.
Configuration
config = RouterConfig(
history_retention_days=90, # drop measurements older than this
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
)
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file multi_user_scale_core-0.1.1.tar.gz.
File metadata
- Download URL: multi_user_scale_core-0.1.1.tar.gz
- Upload date:
- Size: 16.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a1d268e274a6fe4a6d1ff2fc25bbd6b47f09e5f4278ee932a90e2a43f10fc37
|
|
| MD5 |
44eea456d23c060de64ff44e29f8836c
|
|
| BLAKE2b-256 |
68ea065d3e68e201c345a986327f5950ef1b4f502c40e46b17ec02ec082edb79
|
File details
Details for the file multi_user_scale_core-0.1.1-py3-none-any.whl.
File metadata
- Download URL: multi_user_scale_core-0.1.1-py3-none-any.whl
- Upload date:
- Size: 11.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69c4a4e4239fe51ec774c4e21d1a230c0d2aa8cdc4527efa5fd113849364ec6c
|
|
| MD5 |
d48b02b1ec532b1ff71d14c5a938d673
|
|
| BLAKE2b-256 |
4ac6ecb861e05ec3ea137456cd7d1e1010b01429292016484023ece08b03ddca
|