Skip to main content
Version: 3.1

Device

A Device tracks a physical or logical piece of equipment within a facility — from its lifecycle status to where it's placed and which encounter it's attached to.

The Django model is only the storage layer; it types fields loosely — CharField for status enums, JSONField for contact/metadata. The contract lives in the Pydantic resource specs under care/emr/resources/device/, which constrain those CharFields to enums, structure the JSON fields, and define validation, read/write schemas, and server-maintained side effects.

Source:

Models

ModelPurpose
DeviceA physical or logical device tracked within a facility
DeviceEncounterHistoryRecords each period a device was associated with an Encounter
DeviceLocationHistoryRecords each period a device was placed at a FacilityLocation
DeviceServiceHistoryRecords servicing/maintenance events for a device

All models extend EMRBaseModel (shared Care EMR base with external_id, audit fields, and soft-delete semantics).

Device fields

Device data

FieldTypeSpec constraintNotes
identifierCharField(1024)str | NoneFree-form device identifier. Filterable via ?identifier= (case-insensitive exact match)
statusCharField(16)DeviceStatusChoices (required)Lifecycle status — see enum
availability_statusCharField(14)DeviceAvailabilityStatusChoices (required)Physical availability — see enum
manufacturerCharField(1024)str | NoneOptional in the spec, though the model column itself is non-null
manufacture_dateDateTimeFielddatetime | NoneOptional, tz-aware
expiration_dateDateTimeFielddatetime | NoneOptional, tz-aware
lot_numberCharField(1024)str | NoneOptional
serial_numberCharField(1024)str | NoneOptional
registered_nameCharField(1024)str (required)Formal/registered device name; required by every write spec. Searchable via ?search=
user_friendly_nameCharField(1024)str | NoneDisplay name. Searchable via ?search=
model_numberCharField(1024)str | NoneOptional
part_numberCharField(1024)str | NoneOptional
contactJSONField (default {})list[ContactPoint] (default [])A list of structured contact points, not a free-form dict. See ContactPoint shape
care_typeCharField(1024) (default None)str | None (create only)Discriminator selecting a registered device-type plugin. Validated against DeviceTypeRegistry on create — must be a registered key (e.g. "camera") or null. Filterable via ?care_type=
metadataJSONField (default {})not exposed directlyExcluded from all specs. Written server-side by the care_type plugin's handle_create/handle_update. Read it back through the virtual care_metadata field (below)

Virtual / spec-only field

FieldSpec typeNotes
care_metadatadict (default {})Not a DB column. On read it's populated by the care_type plugin — DeviceListSpec calls the plugin's list(obj), DeviceRetrieveSpec calls retrieve(obj). On write it carries plugin-specific data for handle_create/handle_update, which typically persist into the model's metadata column. Falls back to {} when there's no care_type or the plugin lookup fails

Relations

FieldTypeNotes
facilityFK → FacilityCASCADE; owning facility (required). Set server-side from the URL (facility_external_id), never from the request body
managing_organizationFK → FacilityOrganizationSET_NULL, nullable; org responsible for the device. Excluded from the create/update body — managed via dedicated endpoints (see API integration notes)
current_locationFK → FacilityLocationSET_NULL, nullable; where the device is currently placed. Excluded from the create/update body — managed via associate_location
current_encounterFK → EncounterSET_NULL, nullable; encounter the device is currently attached to. Excluded from the create/update body — managed via associate_encounter

Access cache

FieldTypeNotes
facility_organization_cacheArrayField[int]Denormalized cache of facility-organization IDs, rebuilt on every save(). Don't set it directly. get_queryset reads it to scope which devices a user may see

Enum values

DeviceStatusChoices values

status field. Defined in spec.py.

ValueMeaning
activeDevice is in service
inactiveDevice is not currently in service
entered_in_errorRecord created in error

DeviceAvailabilityStatusChoices values

availability_status field. Defined in spec.py.

ValueMeaning
lostDevice cannot be located
damagedDevice is damaged
destroyedDevice is destroyed
availableDevice is available for use

ContactPoint shape

contact is list[ContactPoint]. Each item (from care/emr/resources/common/contact_point.py):

ContactPoint {
system: ContactPointSystemChoices # required
value: str # required
use: ContactPointUseChoices # required
}
system valuesuse values
phone, fax, email, pager, url, sms, otherhome, work, temp, old, mobile

DeviceEncounterHistory

Audit trail of device-to-encounter associations. Attaching a device to an encounter creates a new row; detaching it stamps end. Read-only via the API (EMRModelReadOnlyViewSet), ordered by -end.

device → FK Device (CASCADE)
encounter → FK Encounter (CASCADE)
start → DateTimeField
end → DateTimeField (nullable)

DeviceLocationHistory

Audit trail of device placements. Moving a device to a location creates a new row; leaving it stamps end. Read-only via the API, ordered by -end.

device → FK Device (CASCADE)
location → FK FacilityLocation (CASCADE)
start → DateTimeField
end → DateTimeField (nullable)

DeviceServiceHistory

Servicing/maintenance log for a device. Writable via the API (create + update + list + retrieve).

device → FK Device (PROTECT, required)
serviced_on → DateTimeField (nullable, default None)
note → TextField (default "")
edit_history → JSONField (default [])

device uses PROTECT, so a Device with service-history rows can't be hard-deleted while those rows exist. edit_history is an append-only audit list the server maintains on every update (see Methods & save behaviour).

Resource specs (API schema)

The viewset wires these specs as create = DeviceCreateSpec, update = DeviceUpdateSpec, list = DeviceListSpec, retrieve = DeviceRetrieveSpec. All extend EMRResource (serialize / de_serialize, with perform_extra_serialization / perform_extra_deserialization hooks).

Device specs

Spec classRoleAdds / overridesExcludes
DeviceSpecBaseshared baseid, all device-data fields, status/availability_status enums, contact: list[ContactPoint], registered_name requiredfacility, managing_organization, current_location, current_encounter, care_metadata
DeviceCreateSpecwrite · createcare_type (validated against DeviceTypeRegistry), care_metadata: dict(inherits base excludes)
DeviceUpdateSpecwrite · updatecare_metadata: dict — no care_type, which makes care_type immutable after create(inherits base excludes)
DeviceListSpecread · listid = external_id; care_metadata from plugin list(obj)(inherits)
DeviceRetrieveSpecread · detailfull nested objects: current_location (FacilityLocationListSpec), current_encounter (EncounterListSpec), managing_organization (FacilityOrganizationReadSpec), created_by/updated_by (audit users); care_metadata from plugin retrieve(obj)(inherits)

Device history specs

Spec classRoleFieldsExcludes
DeviceLocationHistoryListSpecread · listid, location (nested FacilityLocationListSpec), created_by, start, end?device, location (raw FK)
DeviceEncounterHistoryListSpecread · listid, encounter (nested EncounterListSpec), created_by, start, end?device, encounter (raw FK)
DeviceServiceHistorySpecBaseshared baseiddevice, edit_history
DeviceServiceHistoryWriteSpecwriteserviced_on: datetime (required), note: str (required)(inherits)
DeviceServiceHistoryListSpecread · listadds created_date, modified_date; id = external_id(inherits)
DeviceServiceHistoryRetrieveSpecread · detailadds edit_history: list[dict] (each entry's updated_by hydrated to a UserSpec from cache), created_by, updated_by(inherits)

Validation rules

  • status and availability_status must each match one of their enum values, or the request is rejected.
  • registered_name is required on every write.
  • care_type (create only) must be a registered device-type key or null. DeviceCreateSpec.validate_care_type calls DeviceTypeRegistry.get_care_device_class(value), which raises for unknown types.
  • care_type can't change on update — DeviceUpdateSpec doesn't expose it.
  • DeviceServiceHistory.serviced_on and note are required on write; updates are blocked once edit_history reaches 50 entries ("Cannot Edit instance anymore").
  • ContactPoint.system, value, and use are all required for each contact entry.

Methods & save behaviour

Device.save() — organization cache

Every save rebuilds facility_organization_cache:

  1. Look up the FacilityOrganization with org_type="root" for the device's facility; its ID seeds the cache.
  2. If managing_organization is set, add its id and its parent_cache (the full ancestor chain).
  3. Persist the resulting set as facility_organization_cache.

This lets the list endpoint filter by organization hierarchy without traversing joins. Treat the cache as platform-maintained; managing_organization and facility are the writable inputs that drive it.

care_type plugin hooks

When care_type is set, the matching DeviceTypeBase subclass from DeviceTypeRegistry runs inside the create/update/delete transaction:

StageHookEffect
createhandle_create(request_data, obj)May mutate obj / obj.metadata and persist (e.g. the bundled camera plugin stores request_data["some_data"] into metadata)
updatehandle_update(request_data, obj)Same, on update
destroyhandle_delete(obj)Validation/cleanup before the device is soft-deleted (deleted=True)
listlist(obj)Supplies care_metadata for DeviceListSpec
retrieveretrieve(obj)Supplies care_metadata for DeviceRetrieveSpec

DeviceServiceHistory edit history

On perform_update, the viewset snapshots the current DB row and appends {serviced_on, note, updated_by} to edit_history before writing the new values — a server-maintained, append-only trail of previous states. Updates are refused once it holds 50 entries.

API integration notes

  • facility comes from the URL (facility_external_id) on create, not from the body.
  • current_location, current_encounter, and managing_organization aren't writable through the device create/update body. Dedicated detail actions mutate them, and each one closes the open history row (stamps end) before opening a new one:
    • POST associate_encounter{ encounter: uuid | null }; closes the open DeviceEncounterHistory, sets current_encounter, and opens (and returns) a new history row. null detaches.
    • POST associate_location{ location: uuid | null }; same pattern against DeviceLocationHistory.
    • POST add_managing_organization{ managing_organization: uuid }.
    • POST remove_managing_organization — clears managing_organization.
  • When an encounter reaches a completed status, disassociate_device_from_encounter clears current_encounter on all attached devices and stamps end on their open DeviceEncounterHistory rows.
  • metadata (the model column) is plugin-owned; clients read deployment-specific data back through the care_metadata virtual field, not metadata.
  • facility_organization_cache is platform-maintained — don't set it directly. The list endpoint scopes results to devices whose cache overlaps the requesting user's facility-organization memberships, or, when ?location= is supplied, by location permission / parent_cache with ?include_children=.