diff --git a/docs/lots.rst b/docs/lots.rst index 0ae8b17f..e3cc83a3 100644 --- a/docs/lots.rst +++ b/docs/lots.rst @@ -27,3 +27,45 @@ To remove children lots the idea is the same: And for devices is all the same: ``POST /lots//devices/?id=&id=``; idem for removing devices. + + +Sharing lots +************ +Sharing a lot means giving certain permissions to users, like reading +the characteristics of devices, or performing certain events. + +Linking lots +============ +Linking lots means setting a reference to a lot in another Devicehub +which supposedly have the same devices. One of both lots is considered +to be the outgoing lot and the other one the ingoing, in the sense +that the lifetime of the devices of the outgoing lot precedes the +incoming lot; in other way, the events of the devices in the outgoing +lot should (but not enforced) happen before that the events in the +incoming lot. + +The lot has two fields that uses to keep a link: the Devicehub URL and +the ID of the other lot (internal ID I suppose...). + +A device can only be in one incoming lot and in one outgoing lot. + +A device inside a linked lot, when it is confirmed the existence of +an equal device in the other DB, is considered to be linked. + +A linked lot defines a both way share of READ Characteristics, at +least. + +Users can sync the characteristics of devices + selected tags + selected rules, +copying (but not deleting) devices between them. This ensures no +traceability warnings when generating the lifetime of devices. In first +iterations this process could be done manually (to allow user select +tags and rules) + + +Why linking lots +---------------- + +* Get the lifetime of devices (reporting). This way a Devicehub + traverses the linked devices (as long as it has perm). +* Send devices to another Devicehub (characteristics). +* Ensure rules over linked devices. diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index 4bbbdc59..f63cd690 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -1,6 +1,7 @@ import csv import pathlib from contextlib import suppress +from fractions import Fraction from itertools import chain from operator import attrgetter from typing import Dict, List, Set @@ -12,6 +13,7 @@ from more_itertools import unique_everseen from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \ Sequence, SmallInteger, Unicode, inspect, text from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import ColumnProperty, backref, relationship, validates from sqlalchemy.util import OrderedSet from sqlalchemy_utils import ColorType @@ -23,8 +25,8 @@ from teal.marshmallow import ValidationError from teal.resource import url_for_resource from ereuse_devicehub.db import db -from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ - PrinterTechnology, RamFormat, RamInterface, Severity +from ereuse_devicehub.resources.enums import BatteryTechnology, ComputerChassis, \ + DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing @@ -58,14 +60,15 @@ class Device(Thing): so it can re-generated *offline*. """ + HID_CONVERSION_DOC - model = Column(Unicode(), check_lower('model')) - model.comment = """The model or brand of the device in lower case. + model = Column(Unicode, check_lower('model')) + model.comment = """The model of the device in lower case. - Devices usually report one of both (model or brand). This value - must be consistent through time. + The model is the unambiguous, as technical as possible, denomination + for the product. This field, among others, is used to identify + the product. """ manufacturer = Column(Unicode(), check_lower('manufacturer')) - manufacturer.comment = """The normalized name of the manufacturer + manufacturer.comment = """The normalized name of the manufacturer, in lower case. Although as of now Devicehub does not enforce normalization, @@ -74,26 +77,38 @@ class Device(Thing): """ serial_number = Column(Unicode(), check_lower('serial_number')) serial_number.comment = """The serial number of the device in lower case.""" + brand = db.Column(CIText()) + brand.comment = """A naming for consumers. This field can represent + several models, so it can be ambiguous, and it is not used to + identify the product. + """ + generation = db.Column(db.SmallInteger, check_range('generation', 0)) + generation.comment = """The generation of the device.""" weight = Column(Float(decimal_return_scale=3), check_range('weight', 0.1, 5)) weight.comment = """ The weight of the device. """ width = Column(Float(decimal_return_scale=3), check_range('width', 0.1, 5)) width.comment = """ - The width of the device. + The width of the device in meters. """ height = Column(Float(decimal_return_scale=3), check_range('height', 0.1, 5)) height.comment = """ - The height of the device. + The height of the device in meters. """ depth = Column(Float(decimal_return_scale=3), check_range('depth', 0.1, 5)) depth.comment = """ - The depth of the device. + The depth of the device in meters. """ color = Column(ColorType) color.comment = """The predominant color of the device.""" - production_date = Column(db.TIMESTAMP(timezone=True)) - production_date.comment = """The date of production of the device.""" + production_date = Column(db.DateTime) + production_date.comment = """The date of production of the device. + This is timezone naive, as Workbench cannot report this data + with timezone information. + """ + variant = Column(Unicode) + variant.comment = """A variant or sub-model of the device.""" _NON_PHYSICAL_PROPS = { 'id', @@ -107,7 +122,11 @@ class Device(Thing): 'width', 'height', 'depth', - 'weight' + 'weight', + 'brand', + 'generation', + 'production_date', + 'variant' } __table_args__ = ( @@ -129,7 +148,7 @@ class Device(Thing): 2. Events performed to a component. 3. Events performed to a parent device. - Events are returned by ascending ``created`` time. + Events are returned by descending ``created`` time. """ return sorted(chain(self.events_multiple, self.events_one), key=self.EVENT_SORT_KEY) @@ -260,6 +279,7 @@ class Device(Thing): :raise LookupError: Device has not an event of the given type. """ try: + # noinspection PyTypeHints return next(e for e in reversed(self.events) if isinstance(e, types)) except StopIteration: raise LookupError('{!r} does not contain events of types {}.'.format(self, types)) @@ -288,12 +308,8 @@ class Device(Thing): class DisplayMixin: - """ - Aspect ratio can be computed as in - https://github.com/mirukan/whratio/blob/master/whratio/ratio.py and - could be a future property. - """ - size = Column(Float(decimal_return_scale=2), check_range('size', 2, 150)) + """Base class for the Display Component and the Monitor Device.""" + size = Column(Float(decimal_return_scale=1), check_range('size', 2, 150), nullable=False) size.comment = """ The size of the monitor in inches. """ @@ -301,27 +317,59 @@ class DisplayMixin: technology.comment = """ The technology the monitor uses to display the image. """ - resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000)) + resolution_width = Column(SmallInteger, check_range('resolution_width', 10, 20000), + nullable=False) resolution_width.comment = """ The maximum horizontal resolution the monitor can natively support in pixels. """ - resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000)) + resolution_height = Column(SmallInteger, check_range('resolution_height', 10, 20000), + nullable=False) resolution_height.comment = """ The maximum vertical resolution the monitor can natively support in pixels. """ refresh_rate = Column(SmallInteger, check_range('refresh_rate', 10, 1000)) contrast_ratio = Column(SmallInteger, check_range('contrast_ratio', 100, 100000)) - touchable = Column(Boolean, nullable=False, default=False) + touchable = Column(Boolean) touchable.comment = """Whether it is a touchscreen.""" + @hybrid_property + def aspect_ratio(self): + """The aspect ratio of the display, as a fraction: ``X/Y``. + + Regular values are ``4/3``, ``5/4``, ``16/9``, ``21/9``, + ``14/10``, ``19/10``, ``16/10``. + """ + return Fraction(self.resolution_width, self.resolution_height) + + # noinspection PyUnresolvedReferences + @aspect_ratio.expression + def aspect_ratio(cls): + # The aspect ratio to use as SQL in the DB + # This allows comparing resolutions + return db.func.round(cls.resolution_width / cls.resolution_height, 2) + + @hybrid_property + def widescreen(self): + """Whether the monitor is considered to be widescreen. + + Widescreen monitors are those having a higher aspect ratio + greater than 4/3. + """ + # We add a tiny extra to 4/3 to avoid precision errors + return self.aspect_ratio > 4.001 / 3 + + def __str__(self) -> str: + return '{0.t} {0.serial_number} {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self) + def __format__(self, format_spec: str) -> str: v = '' if 't' in format_spec: v += '{0.t} {0.model}'.format(self) if 's' in format_spec: - v += '({0.manufacturer}) S/N {0.serial_number} – {0.size}in {0.technology}' + v += '({0.manufacturer}) S/N {0.serial_number}'.format(self) + v += '– {0.size}in ({0.aspect_ratio}) {0.technology}'.format(self) return v @@ -443,14 +491,16 @@ class Mobile(Device): id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) imei = Column(BigInteger) - imei.comment = """ - The International Mobile Equipment Identity of the smartphone - as an integer. + imei.comment = """The International Mobile Equipment Identity of + the smartphone as an integer. """ meid = Column(Unicode) - meid.comment = """ - The Mobile Equipment Identifier as a hexadecimal string. + meid.comment = """The Mobile Equipment Identifier as a hexadecimal + string. """ + ram_size = db.Column(db.Integer, check_range(1, )) + ram_size.comment = """The total of RAM of the device in MB.""" + data_storage_size = db.Column(db.Integer) @validates('imei') def validate_imei(self, _, value: int): @@ -576,6 +626,8 @@ class Motherboard(JoinedComponentTableMixin, Component): firewire = Column(SmallInteger, check_range('firewire', min=0)) serial = Column(SmallInteger, check_range('serial', min=0)) pcmcia = Column(SmallInteger, check_range('pcmcia', min=0)) + bios_date = Column(db.Date) + bios_date.comment = """The date of the BIOS version.""" class NetworkMixin: @@ -609,6 +661,8 @@ class Processor(JoinedComponentTableMixin, Component): threads.comment = """The number of threads per core.""" address = Column(SmallInteger, check_range('address', 8, 256)) address.comment = """The address of the CPU: 8, 16, 32, 64, 128 or 256 bits.""" + abi = Column(Unicode, check_lower('abi')) + abi.comment = """The Application Binary Interface of the processor.""" class RamModule(JoinedComponentTableMixin, Component): @@ -634,6 +688,24 @@ class Display(JoinedComponentTableMixin, DisplayMixin, Component): pass +class Battery(JoinedComponentTableMixin, Component): + wireless = db.Column(db.Boolean) + wireless.comment = """If the battery can be charged wirelessly.""" + technology = db.Column(db.Enum(BatteryTechnology)) + size = db.Column(db.Integer, nullable=False) + size.comment = """Maximum battery capacity by design, in mAh. + + Use BatteryTest's "size" to get the actual size of the battery. + """ + + @property + def capacity(self) -> float: + """The quantity of """ + from ereuse_devicehub.resources.event.models import MeasureBattery + real_size = self.last_event_of(MeasureBattery).size + return real_size / self.size if real_size and self.size else None + + class ComputerAccessory(Device): """Computer peripherals and similar accessories.""" id = Column(BigInteger, ForeignKey(Device.id), primary_key=True) diff --git a/ereuse_devicehub/resources/device/models.pyi b/ereuse_devicehub/resources/device/models.pyi index 962e4901..cf5b2c34 100644 --- a/ereuse_devicehub/resources/device/models.pyi +++ b/ereuse_devicehub/resources/device/models.pyi @@ -1,6 +1,7 @@ from datetime import datetime +from fractions import Fraction from operator import attrgetter -from typing import Dict, Generator, Iterable, List, Optional, Set, Type +from typing import Dict, Generator, Iterable, List, Optional, Set, Type, TypeVar from boltons import urlutils from boltons.urlutils import URL @@ -12,15 +13,16 @@ from teal.enums import Layouts from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.device import states -from ereuse_devicehub.resources.enums import ComputerChassis, DataStorageInterface, DisplayTech, \ - PrinterTechnology, RamFormat, RamInterface +from ereuse_devicehub.resources.enums import BatteryTechnology, ComputerChassis, \ + DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface from ereuse_devicehub.resources.event import models as e -from ereuse_devicehub.resources.image.models import ImageList from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.tag import Tag from ereuse_devicehub.resources.tag.model import Tags +E = TypeVar('E', bound=e.Event) + class Device(Thing): EVENT_SORT_KEY = attrgetter('created') @@ -38,27 +40,32 @@ class Device(Thing): color = ... # type: Column lots = ... # type: relationship production_date = ... # type: Column + brand = ... # type: Column + generation = ... # type: Column + variant = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self.id = ... # type: int self.type = ... # type: str - self.hid = ... # type: str - self.model = ... # type: str - self.manufacturer = ... # type: str - self.serial_number = ... # type: str - self.weight = ... # type: float - self.width = ... # type:float - self.height = ... # type: float - self.depth = ... # type: float - self.color = ... # type: Color + self.hid = ... # type: Optional[str] + self.model = ... # type: Optional[str] + self.manufacturer = ... # type: Optional[str] + self.serial_number = ... # type: Optional[str] + self.weight = ... # type: Optional[float] + self.width = ... # type:Optional[float] + self.height = ... # type: Optional[float] + self.depth = ... # type: Optional[float] + self.color = ... # type: Optional[Color] self.physical_properties = ... # type: Dict[str, object or None] self.events_multiple = ... # type: Set[e.EventWithMultipleDevices] self.events_one = ... # type: Set[e.EventWithOneDevice] - self.images = ... # type: ImageList self.tags = ... # type: Tags[Tag] self.lots = ... # type: Set[Lot] - self.production_date = ... # type: datetime + self.production_date = ... # type: Optional[datetime] + self.brand = ... # type: Optional[str] + self.generation = ... # type: Optional[int] + self.variant = ... # type: Optional[str] @property def events(self) -> List[e.Event]: @@ -96,7 +103,7 @@ class Device(Thing): def working(self) -> List[e.Test]: pass - def last_event_of(self, *types: Type[e.Event]) -> e.Event: + def last_event_of(self, *types: Type[E]) -> E: pass def _warning_events(self, events: Iterable[e.Event]) -> Generator[e.Event]: @@ -118,9 +125,11 @@ class DisplayMixin: self.size = ... # type: Integer self.resolution_width = ... # type: int self.resolution_height = ... # type: int - self.refresh_rate = ... # type: int - self.contrast_ratio = ... # type: int - self.touchable = ... # type: bool + self.refresh_rate = ... # type: Optional[int] + self.contrast_ratio = ... # type: Optional[int] + self.touchable = ... # type: Optional[bool] + self.aspect_ratio = ... #type: Fraction + self.widescreen = ... # type: bool class Computer(DisplayMixin, Device): @@ -193,11 +202,15 @@ class TelevisionSet(Monitor): class Mobile(Device): imei = ... # type: Column meid = ... # type: Column + ram_size = ... # type: Column + data_storage_size = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - self.imei = ... # type: int - self.meid = ... # type: str + self.imei = ... # type: Optional[int] + self.meid = ... # type: Optional[str] + self.ram_size = ... # type: Optional[int] + self.data_storage_size = ... # type: Optional[int] class Smartphone(Mobile): @@ -288,13 +301,15 @@ class Processor(Component): cores = ... # type: Column address = ... # type: Column threads = ... # type: Column + abi = ... # type: Column def __init__(self, **kwargs) -> None: super().__init__(**kwargs) - self.speed = ... # type: float - self.cores = ... # type: int - self.threads = ... # type: int - self.address = ... # type: int + self.speed = ... # type: Optional[float] + self.cores = ... # type: Optional[int] + self.threads = ... # type: Optional[int] + self.address = ... # type: Optional[int] + self.abi = ... # type: Optional[str] class RamModule(Component): @@ -319,6 +334,18 @@ class Display(DisplayMixin, Component): pass +class Battery(Component): + wireless = ... # type: Column + technology = ... # type: Column + size = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.wireless = ... # type: Optional[bool] + self.technology = ... # type: Optional[BatteryTechnology] + self.size = ... # type: bool + + class ComputerAccessory(Device): pass diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 9b3eb5b2..eab461c1 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -22,9 +22,17 @@ class Device(Thing): many=True, collection_class=OrderedSet, description='A set of tags that identify the device.') - model = SanitizedStr(lower=True, validate=Length(max=STR_BIG_SIZE)) - manufacturer = SanitizedStr(lower=True, validate=Length(max=STR_SIZE)) - serial_number = SanitizedStr(lower=True, data_key='serialNumber') + model = SanitizedStr(lower=True, + validate=Length(max=STR_BIG_SIZE), + description=m.Device.model.comment) + manufacturer = SanitizedStr(lower=True, + validate=Length(max=STR_SIZE), + description=m.Device.manufacturer.comment) + serial_number = SanitizedStr(lower=True, + validate=Length(max=STR_BIG_SIZE), + data_key='serialNumber') + brand = SanitizedStr(validate=Length(max=STR_BIG_SIZE), description=m.Device.brand.comment) + generation = Integer(validate=Range(1, 100), description=m.Device.generation.comment) weight = Float(validate=Range(0.1, 5), unit=UnitCodes.kgm, description=m.Device.weight.comment) width = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.width.comment) height = Float(validate=Range(0.1, 5), unit=UnitCodes.m, description=m.Device.height.comment) @@ -113,8 +121,8 @@ class Desktop(Computer): class Laptop(Computer): - layout = EnumField(Layouts, description=m.Laptop.layout.comment) __doc__ = m.Laptop.__doc__ + layout = EnumField(Layouts, description=m.Laptop.layout.comment) class Server(Computer): @@ -123,19 +131,22 @@ class Server(Computer): class DisplayMixin: __doc__ = m.DisplayMixin.__doc__ - - size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150)) + size = Float(description=m.DisplayMixin.size.comment, validate=Range(2, 150), required=True) technology = EnumField(enums.DisplayTech, description=m.DisplayMixin.technology.comment) resolution_width = Integer(data_key='resolutionWidth', validate=Range(10, 20000), - description=m.DisplayMixin.resolution_width.comment) + description=m.DisplayMixin.resolution_width.comment, + required=True) resolution_height = Integer(data_key='resolutionHeight', validate=Range(10, 20000), - description=m.DisplayMixin.resolution_height.comment) + description=m.DisplayMixin.resolution_height.comment, + required=True) refresh_rate = Integer(data_key='refreshRate', validate=Range(10, 1000)) contrast_ratio = Integer(data_key='contrastRatio', validate=Range(100, 100000)) - touchable = Boolean(missing=False, description=m.DisplayMixin.touchable.comment) + touchable = Boolean(description=m.DisplayMixin.touchable.comment) + aspect_ratio = String(dump_only=True, description=m.DisplayMixin.aspect_ratio.__doc__) + widescreen = Boolean(dump_only=True, description=m.DisplayMixin.widescreen.__doc__) class NetworkMixin: @@ -164,6 +175,14 @@ class Mobile(Device): imei = Integer(description=m.Mobile.imei.comment) meid = Str(description=m.Mobile.meid.comment) + ram_size = Integer(validate=Range(min=128, max=36000), + data_key='ramSize', + unit=UnitCodes.mbyte, + description=m.Mobile.ram_size.comment) + data_storage_size = Integer(validate=Range(0, 10 ** 8), + data_key='dataStorageSize', + description=m.Mobile.data_storage_size) + @pre_load def convert_check_imei(self, data): @@ -247,6 +266,7 @@ class Processor(Component): threads = Integer(validate=Range(min=1, max=20), description=m.Processor.threads.comment) address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256}), description=m.Processor.address.comment) + abi = SanitizedStr(lower=True, description=m.Processor.abi.comment) class RamModule(Component): @@ -268,6 +288,14 @@ class Display(DisplayMixin, Component): __doc__ = m.Display.__doc__ +class Battery(Component): + __doc__ = m.Battery + + wireless = Boolean(description=m.Battery.wireless.comment) + technology = EnumField(enums.BatteryTechnology, description=m.Battery.technology.comment) + size = Integer(required=True, description=m.Battery.size.comment) + + class Manufacturer(Schema): __doc__ = m.Manufacturer.__doc__ diff --git a/ereuse_devicehub/resources/enums.py b/ereuse_devicehub/resources/enums.py index 78c4e6a3..672a96ea 100644 --- a/ereuse_devicehub/resources/enums.py +++ b/ereuse_devicehub/resources/enums.py @@ -276,6 +276,26 @@ class PrinterTechnology(Enum): Thermal = 'Thermal' +@unique +class BatteryHealth(Enum): + """The battery health status as in Android.""" + Cold = 'Cold' + Dead = 'Dead' + Good = 'Good' + Overheat = 'Overheat' + OverVoltage = 'OverVoltage' + UnspecifiedValue = 'UnspecifiedValue' + + +@unique +class BatteryTechnology(Enum): + """The technology of the Battery.""" + LiIon = 'Lithium-ion' + NiCad = 'Nickel-Cadmium' + NiMH = 'Nickel-metal hydride' + Al = 'Alkaline' + + class Severity(IntEnum): """A flag evaluating the event execution. Ex. failed events have the value `Severity.Error`. Devicehub uses 4 severity levels: diff --git a/ereuse_devicehub/resources/event/models.py b/ereuse_devicehub/resources/event/models.py index 946a97b8..451b7ef8 100644 --- a/ereuse_devicehub/resources/event/models.py +++ b/ereuse_devicehub/resources/event/models.py @@ -28,10 +28,10 @@ from ereuse_devicehub.db import db from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.device.models import Component, Computer, DataStorage, Desktop, \ Device, Laptop, Server -from ereuse_devicehub.resources.enums import AppearanceRange, Bios, ErasureStandards, \ - FunctionalityRange, PhysicalErasureMethod, PriceSoftware, RATE_NEGATIVE, RATE_POSITIVE, \ - RatingRange, RatingSoftware, ReceiverRole, Severity, SnapshotExpectedEvents, SnapshotSoftware, \ - TestDataStorageLength +from ereuse_devicehub.resources.enums import AppearanceRange, BatteryHealth, Bios, \ + ErasureStandards, FunctionalityRange, PhysicalErasureMethod, PriceSoftware, RATE_NEGATIVE, \ + RATE_POSITIVE, RatingRange, RatingSoftware, ReceiverRole, Severity, SnapshotExpectedEvents, \ + SnapshotSoftware, TestDataStorageLength from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing from ereuse_devicehub.resources.user.models import User @@ -384,7 +384,9 @@ class ErasePhysical(EraseBasic): class Step(db.Model): - erasure_id = Column(UUID(as_uuid=True), ForeignKey(EraseBasic.id), primary_key=True) + erasure_id = Column(UUID(as_uuid=True), + ForeignKey(EraseBasic.id, ondelete='CASCADE'), + primary_key=True) type = Column(Unicode(STR_SM_SIZE), nullable=False) num = Column(SmallInteger, primary_key=True) severity = Column(teal.db.IntEnum(Severity), default=Severity.Info, nullable=False) @@ -977,7 +979,13 @@ class Test(JoinedWithOneDeviceMixin, EventWithOneDevice): return args -class TestDataStorage(Test): +class TestMixin: + @declared_attr + def id(cls): + return Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True) + + +class TestDataStorage(TestMixin, Test): """ The act of testing the data storage. @@ -989,7 +997,6 @@ class TestDataStorage(Test): The test takes to other SMART values indicators of the overall health of the data storage. """ - id = Column(UUID(as_uuid=True), ForeignKey(Test.id), primary_key=True) length = Column(DBEnum(TestDataStorageLength), nullable=False) # todo from type status = Column(Unicode(), check_lower('status'), nullable=False) lifetime = Column(Interval) @@ -1034,6 +1041,25 @@ class TestDataStorage(Test): self._reported_uncorrectable_errors = min(value, db.PSQL_INT_MAX) +class MeasureBattery(TestMixin, Test): + """A sample of the status of the battery. + + Operative Systems keep a record of several aspects of a battery. + This is a sample of those. + """ + size = db.Column(db.Integer, nullable=False) + size.comment = """Maximum battery capacity, in mAh.""" + voltage = db.Column(db.Integer, nullable=False) + voltage.comment = """The actual voltage of the battery, in mV.""" + cycle_count = db.Column(db.Integer) + cycle_count.comment = """The number of full charges – discharges + cycles. + """ + health = db.Column(db.Enum(BatteryHealth)) + health.comment = """The health of the Battery. + Only reported in Android. + """ + class StressTest(Test): """The act of stressing (putting to the maximum capacity) @@ -1071,9 +1097,14 @@ class Benchmark(JoinedWithOneDeviceMixin, EventWithOneDevice): return args -class BenchmarkDataStorage(Benchmark): +class BenchmarkMixin: + @declared_attr + def id(cls): + return Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True) + + +class BenchmarkDataStorage(BenchmarkMixin, Benchmark): """Benchmarks the data storage unit reading and writing speeds.""" - id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True) read_speed = Column(Float(decimal_return_scale=2), nullable=False) write_speed = Column(Float(decimal_return_scale=2), nullable=False) @@ -1081,9 +1112,8 @@ class BenchmarkDataStorage(Benchmark): return 'Read: {} MB/s, write: {} MB/s'.format(self.read_speed, self.write_speed) -class BenchmarkWithRate(Benchmark): +class BenchmarkWithRate(BenchmarkMixin, Benchmark): """The act of benchmarking a device with a single rate.""" - id = Column(UUID(as_uuid=True), ForeignKey(Benchmark.id), primary_key=True) rate = Column(Float, nullable=False) def __str__(self) -> str: diff --git a/ereuse_devicehub/resources/event/models.pyi b/ereuse_devicehub/resources/event/models.pyi index c6340b8c..04c86d98 100644 --- a/ereuse_devicehub/resources/event/models.pyi +++ b/ereuse_devicehub/resources/event/models.pyi @@ -16,9 +16,9 @@ from teal.enums import Country from ereuse_devicehub.resources.agent.models import Agent from ereuse_devicehub.resources.device.models import Component, Computer, Device -from ereuse_devicehub.resources.enums import AppearanceRange, Bios, ErasureStandards, \ - FunctionalityRange, PhysicalErasureMethod, PriceSoftware, RatingSoftware, ReceiverRole, \ - Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength +from ereuse_devicehub.resources.enums import AppearanceRange, BatteryHealth, Bios, \ + ErasureStandards, FunctionalityRange, PhysicalErasureMethod, PriceSoftware, RatingSoftware, \ + ReceiverRole, Severity, SnapshotExpectedEvents, SnapshotSoftware, TestDataStorageLength from ereuse_devicehub.resources.models import Thing from ereuse_devicehub.resources.user.models import User @@ -357,6 +357,20 @@ class TestDataStorage(Test): self.remaining_lifetime_percentage = ... # type: int +class MeasureBattery(Test): + size = ... # type: Column + voltage = ... # type: Column + cycle_count = ... # type: Column + health = ... # type: Column + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self.size = ... # type: int + self.voltage = ... # type: int + self.cycle_count = ... # type: Optional[int] + self.health = ... # type: Optional[BatteryHealth] + + class StressTest(Test): pass diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index 5a1a2363..790bd0c6 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -310,6 +310,14 @@ class TestDataStorage(Test): remaining_lifetime_percentage = Integer(data_key='remainingLifetimePercentage') +class MeasureBattery(Test): + __doc__ = m.MeasureBattery.__doc__ + size = Integer(required=True, description=m.MeasureBattery.size.comment) + voltage = Integer(required=True, description=m.MeasureBattery.voltage.comment) + cycle_count = Integer(required=True, description=m.MeasureBattery.cycle_count.comment) + health = EnumField(enums.BatteryHealth, description=m.MeasureBattery.health.comment) + + class StressTest(Test): __doc__ = m.StressTest.__doc__ diff --git a/tests/test_device.py b/tests/test_device.py index f3ea0c6a..a780ec36 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -116,6 +116,7 @@ def test_physical_properties(): 'serial': None, 'firewire': None, 'manufacturer': 'mr', + 'bios_date': None } assert pc.physical_properties == { 'model': 'foo', @@ -454,11 +455,6 @@ def test_computer_monitor(): db.session.commit() -@pytest.mark.xfail(reason='Make test') -def test_computer_with_display(): - pass - - def test_manufacturer(user: UserClient): m, r = user.get(res='Manufacturer', query=[('search', 'asus')]) assert m == {'items': [{'name': 'Asus', 'url': 'https://en.wikipedia.org/wiki/Asus'}]}