Spool weight now counts upwards instead

The used filament weight is now kept track of instead of remaining weight. This adds robustness to measurement errors which could accumulate and then the filament usage wouldn't get tracked anymore once the weight hit 0. Now it will just keep going.
This commit is contained in:
Donkie
2023-04-06 20:06:03 +02:00
parent 1a148c0991
commit 8a5d2e220b
4 changed files with 83 additions and 31 deletions

View File

@@ -91,7 +91,15 @@ class Spool(BaseModel):
first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.") first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.")
last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage.") last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage.")
filament: Filament = Field(description="The filament type of this spool.") filament: Filament = Field(description="The filament type of this spool.")
weight: float = Field(ge=0, description="Remaining weight of filament on the spool.", example=500) remaining_weight: Optional[float] = Field(
default=None,
ge=0,
description=(
"Estimated remaining weight of filament on the spool. Only set if the filament type has a weight set."
),
example=500,
)
used_weight: float = Field(ge=0, description="Consumed weight of filament from the spool.", example=500)
location: Optional[str] = Field(max_length=64, description="Where this spool can be found.", example="Shelf A") location: Optional[str] = Field(max_length=64, description="Where this spool can be found.", example="Shelf A")
lot_nr: Optional[str] = Field( lot_nr: Optional[str] = Field(
max_length=64, max_length=64,
@@ -107,13 +115,19 @@ class Spool(BaseModel):
@staticmethod @staticmethod
def from_db(item: models.Spool) -> "Spool": def from_db(item: models.Spool) -> "Spool":
"""Create a new Pydantic spool object from a database spool object.""" """Create a new Pydantic spool object from a database spool object."""
filament = Filament.from_db(item.filament)
remaining_weight: Optional[float] = None
if filament.weight is not None:
remaining_weight = max(filament.weight - item.used_weight, 0)
return Spool( return Spool(
id=item.id, id=item.id,
registered=item.registered, registered=item.registered,
first_used=item.first_used, first_used=item.first_used,
last_used=item.last_used, last_used=item.last_used,
filament=Filament.from_db(item.filament), filament=filament,
weight=item.weight, used_weight=item.used_weight,
remaining_weight=remaining_weight,
location=item.location, location=item.location,
lot_nr=item.lot_nr, lot_nr=item.lot_nr,
comment=item.comment, comment=item.comment,

View File

@@ -27,14 +27,14 @@ class SpoolParameters(BaseModel):
first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.") first_used: Optional[datetime] = Field(description="First logged occurence of spool usage.")
last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage.") last_used: Optional[datetime] = Field(description="Last logged occurence of spool usage.")
filament_id: int = Field(description="The ID of the filament type of this spool.") filament_id: int = Field(description="The ID of the filament type of this spool.")
weight: Optional[float] = Field( remaining_weight: Optional[float] = Field(
ge=0, ge=0,
description=( description=(
"Remaining weight of filament on the spool. " "Remaining weight of filament on the spool. Can only be used if the filament type has a weight set."
"Leave empty to assume a full spool based on the weight parameters of the filament type."
), ),
example=500, example=800,
) )
used_weight: Optional[float] = Field(ge=0, description="Used weight of filament on the spool.", example=200)
location: Optional[str] = Field(max_length=64, description="Where this spool can be found.", example="Shelf A") location: Optional[str] = Field(max_length=64, description="Where this spool can be found.", example="Shelf A")
lot_nr: Optional[str] = Field( lot_nr: Optional[str] = Field(
max_length=64, max_length=64,
@@ -132,7 +132,11 @@ async def get(
@router.post( @router.post(
"/", "/",
name="Add spool", name="Add spool",
description="Add a new spool to the database.", description=(
"Add a new spool to the database. "
"Only specify either remaining_weight or used_weight. "
"If no weight is set, the spool will be assumed to be full."
),
response_model_exclude_none=True, response_model_exclude_none=True,
response_model=Spool, response_model=Spool,
responses={ responses={
@@ -143,11 +147,18 @@ async def create( # noqa: ANN201
db: Annotated[AsyncSession, Depends(get_db_session)], db: Annotated[AsyncSession, Depends(get_db_session)],
body: SpoolParameters, body: SpoolParameters,
): ):
if body.remaining_weight is not None and body.used_weight is not None:
return JSONResponse(
status_code=400,
content={"message": "Only specify either remaining_weight or used_weight."},
)
try: try:
db_item = await spool.create( db_item = await spool.create(
db=db, db=db,
filament_id=body.filament_id, filament_id=body.filament_id,
weight=body.weight, remaining_weight=body.remaining_weight,
used_weight=body.used_weight,
first_used=body.first_used, first_used=body.first_used,
last_used=body.last_used, last_used=body.last_used,
location=body.location, location=body.location,
@@ -165,26 +176,46 @@ async def create( # noqa: ANN201
@router.patch( @router.patch(
"/{spool_id}", "/{spool_id}",
name="Update spool", name="Update spool",
description="Update any attribute of a spool. Only fields specified in the request will be affected.", description=(
"Update any attribute of a spool. "
"Only fields specified in the request will be affected. "
"remaining_weight and used_weight can't be set at the same time."
),
response_model_exclude_none=True, response_model_exclude_none=True,
response_model=Spool,
responses={
400: {"model": Message},
},
) )
async def update( async def update( # noqa: ANN201
db: Annotated[AsyncSession, Depends(get_db_session)], db: Annotated[AsyncSession, Depends(get_db_session)],
spool_id: int, spool_id: int,
body: SpoolUpdateParameters, body: SpoolUpdateParameters,
) -> Spool: ):
patch_data = body.dict(exclude_unset=True) patch_data = body.dict(exclude_unset=True)
if body.remaining_weight is not None and body.used_weight is not None:
return JSONResponse(
status_code=400,
content={"message": "Only specify either remaining_weight or used_weight."},
)
if "filament_id" in patch_data and body.filament_id is None: if "filament_id" in patch_data and body.filament_id is None:
raise RequestValidationError( raise RequestValidationError(
[ErrorWrapper(ValueError("filament_id cannot be unset"), ("query", "filament_id"))], [ErrorWrapper(ValueError("filament_id cannot be unset"), ("query", "filament_id"))],
) )
db_item = await spool.update( try:
db=db, db_item = await spool.update(
spool_id=spool_id, db=db,
data=patch_data, spool_id=spool_id,
) data=patch_data,
)
except ItemCreateError as exc:
return JSONResponse(
status_code=400,
content={"message": str(exc)},
)
return Spool.from_db(db_item) return Spool.from_db(db_item)

View File

@@ -50,7 +50,7 @@ class Spool(Base):
last_used: Mapped[Optional[datetime]] = mapped_column() last_used: Mapped[Optional[datetime]] = mapped_column()
filament_id: Mapped[int] = mapped_column(ForeignKey("filament.id")) filament_id: Mapped[int] = mapped_column(ForeignKey("filament.id"))
filament: Mapped["Filament"] = relationship(back_populates="spools") filament: Mapped["Filament"] = relationship(back_populates="spools")
weight: Mapped[float] = mapped_column() used_weight: Mapped[float] = mapped_column()
location: Mapped[Optional[str]] = mapped_column(String(64)) location: Mapped[Optional[str]] = mapped_column(String(64))
lot_nr: Mapped[Optional[str]] = mapped_column(String(64)) lot_nr: Mapped[Optional[str]] = mapped_column(String(64))
comment: Mapped[Optional[str]] = mapped_column(String(1024)) comment: Mapped[Optional[str]] = mapped_column(String(1024))

View File

@@ -16,7 +16,8 @@ async def create(
*, *,
db: AsyncSession, db: AsyncSession,
filament_id: int, filament_id: int,
weight: Optional[float] = None, remaining_weight: Optional[float] = None,
used_weight: Optional[float] = None,
first_used: Optional[datetime] = None, first_used: Optional[datetime] = None,
last_used: Optional[datetime] = None, last_used: Optional[datetime] = None,
location: Optional[str] = None, location: Optional[str] = None,
@@ -25,14 +26,17 @@ async def create(
) -> models.Spool: ) -> models.Spool:
"""Add a new spool to the database. Leave weight empty to assume full spool.""" """Add a new spool to the database. Leave weight empty to assume full spool."""
filament_item = await filament.get_by_id(db, filament_id) filament_item = await filament.get_by_id(db, filament_id)
if weight is None: if used_weight is None:
if filament_item.weight is None: if remaining_weight is not None:
raise ItemCreateError("The weight for neither the spool nor its filament type is set.") if filament_item.weight is None:
weight = filament_item.weight raise ItemCreateError("remaining_weight can only be used if the filament type has a weight set.")
used_weight = max(filament_item.weight - remaining_weight, 0)
else:
used_weight = 0
db_item = models.Spool( db_item = models.Spool(
filament=filament_item, filament=filament_item,
weight=weight, used_weight=used_weight,
first_used=first_used, first_used=first_used,
last_used=last_used, last_used=last_used,
location=location, location=location,
@@ -105,6 +109,10 @@ async def update(
for k, v in data.items(): for k, v in data.items():
if k == "filament_id": if k == "filament_id":
spool.filament = await filament.get_by_id(db, v) spool.filament = await filament.get_by_id(db, v)
elif k == "remaining_weight":
if spool.filament.weight is None:
raise ItemCreateError("remaining_weight can only be used if the filament type has a weight set.")
spool.used_weight = max(spool.filament.weight - v, 0)
else: else:
setattr(spool, k, v) setattr(spool, k, v)
await db.flush() await db.flush()
@@ -119,33 +127,33 @@ async def delete(db: AsyncSession, spool_id: int) -> None:
# TODO: Make unit tests for race conditions on these # TODO: Make unit tests for race conditions on these
async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.Spool: async def use_weight(db: AsyncSession, spool_id: int, weight: float) -> models.Spool:
"""Reduce the weight of a spool. """Consume filament from a spool by weight.
Does nothing if the spool is empty. Does nothing if the spool is empty.
Args: Args:
db (AsyncSession): Database session db (AsyncSession): Database session
spool_id (int): Spool ID spool_id (int): Spool ID
weight (float): Weight loss in grams weight (float): Filament weight to consume, in grams
Returns: Returns:
models.Spool: Updated spool object models.Spool: Updated spool object
""" """
spool = await get_by_id(db, spool_id, with_for_update=True) spool = await get_by_id(db, spool_id, with_for_update=True)
spool.weight -= min(spool.weight, weight) spool.used_weight += weight
await db.flush() await db.flush()
return spool return spool
async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.Spool: async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.Spool:
"""Reduce the weight of a spool by using a length of filament. """Consume filament from a spool by length.
Does nothing if the spool is empty. Does nothing if the spool is empty.
Args: Args:
db (AsyncSession): Database session db (AsyncSession): Database session
spool_id (int): Spool ID spool_id (int): Spool ID
length (float): Length of filament to reduce by length (float): Length of filament to consume, in mm
Returns: Returns:
models.Spool: Updated spool object models.Spool: Updated spool object
@@ -154,11 +162,10 @@ async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.S
filament = spool.filament filament = spool.filament
weight = weight_from_length( spool.used_weight += weight_from_length(
length=length, length=length,
radius=filament.diameter / 2, radius=filament.diameter / 2,
density=filament.density, density=filament.density,
) )
spool.weight -= min(spool.weight, weight)
await db.flush() await db.flush()
return spool return spool