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:
- Model:
care/emr/models/device.py - Spec:
care/emr/resources/device/spec.py - History spec:
care/emr/resources/device/history_spec.py - Viewset:
care/emr/api/viewsets/device.py - Device-type registry:
care/emr/registries/device_type/device_registry.py
Models
| Model | Purpose |
|---|---|
Device | A physical or logical device tracked within a facility |
DeviceEncounterHistory | Records each period a device was associated with an Encounter |
DeviceLocationHistory | Records each period a device was placed at a FacilityLocation |
DeviceServiceHistory | Records 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
| Field | Type | Spec constraint | Notes |
|---|---|---|---|
identifier | CharField(1024) | str | None | Free-form device identifier. Filterable via ?identifier= (case-insensitive exact match) |
status | CharField(16) | DeviceStatusChoices (required) | Lifecycle status — see enum |
availability_status | CharField(14) | DeviceAvailabilityStatusChoices (required) | Physical availability — see enum |
manufacturer | CharField(1024) | str | None | Optional in the spec, though the model column itself is non-null |
manufacture_date | DateTimeField | datetime | None | Optional, tz-aware |
expiration_date | DateTimeField | datetime | None | Optional, tz-aware |
lot_number | CharField(1024) | str | None | Optional |
serial_number | CharField(1024) | str | None | Optional |
registered_name | CharField(1024) | str (required) | Formal/registered device name; required by every write spec. Searchable via ?search= |
user_friendly_name | CharField(1024) | str | None | Display name. Searchable via ?search= |
model_number | CharField(1024) | str | None | Optional |
part_number | CharField(1024) | str | None | Optional |
contact | JSONField (default {}) | list[ContactPoint] (default []) | A list of structured contact points, not a free-form dict. See ContactPoint shape |
care_type | CharField(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= |
metadata | JSONField (default {}) | not exposed directly | Excluded 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
| Field | Spec type | Notes |
|---|---|---|
care_metadata | dict (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
| Field | Type | Notes |
|---|---|---|
facility | FK → Facility | CASCADE; owning facility (required). Set server-side from the URL (facility_external_id), never from the request body |
managing_organization | FK → FacilityOrganization | SET_NULL, nullable; org responsible for the device. Excluded from the create/update body — managed via dedicated endpoints (see API integration notes) |
current_location | FK → FacilityLocation | SET_NULL, nullable; where the device is currently placed. Excluded from the create/update body — managed via associate_location |
current_encounter | FK → Encounter | SET_NULL, nullable; encounter the device is currently attached to. Excluded from the create/update body — managed via associate_encounter |
Access cache
| Field | Type | Notes |
|---|---|---|
facility_organization_cache | ArrayField[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.
| Value | Meaning |
|---|---|
active | Device is in service |
inactive | Device is not currently in service |
entered_in_error | Record created in error |
DeviceAvailabilityStatusChoices values
availability_status field. Defined in spec.py.
| Value | Meaning |
|---|---|
lost | Device cannot be located |
damaged | Device is damaged |
destroyed | Device is destroyed |
available | Device 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 values | use values |
|---|---|
phone, fax, email, pager, url, sms, other | home, work, temp, old, mobile |
Related models
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 class | Role | Adds / overrides | Excludes |
|---|---|---|---|
DeviceSpecBase | shared base | id, all device-data fields, status/availability_status enums, contact: list[ContactPoint], registered_name required | facility, managing_organization, current_location, current_encounter, care_metadata |
DeviceCreateSpec | write · create | care_type (validated against DeviceTypeRegistry), care_metadata: dict | (inherits base excludes) |
DeviceUpdateSpec | write · update | care_metadata: dict — no care_type, which makes care_type immutable after create | (inherits base excludes) |
DeviceListSpec | read · list | id = external_id; care_metadata from plugin list(obj) | (inherits) |
DeviceRetrieveSpec | read · detail | full 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 class | Role | Fields | Excludes |
|---|---|---|---|
DeviceLocationHistoryListSpec | read · list | id, location (nested FacilityLocationListSpec), created_by, start, end? | device, location (raw FK) |
DeviceEncounterHistoryListSpec | read · list | id, encounter (nested EncounterListSpec), created_by, start, end? | device, encounter (raw FK) |
DeviceServiceHistorySpecBase | shared base | id | device, edit_history |
DeviceServiceHistoryWriteSpec | write | serviced_on: datetime (required), note: str (required) | (inherits) |
DeviceServiceHistoryListSpec | read · list | adds created_date, modified_date; id = external_id | (inherits) |
DeviceServiceHistoryRetrieveSpec | read · detail | adds edit_history: list[dict] (each entry's updated_by hydrated to a UserSpec from cache), created_by, updated_by | (inherits) |
Validation rules
statusandavailability_statusmust each match one of their enum values, or the request is rejected.registered_nameis required on every write.care_type(create only) must be a registered device-type key ornull.DeviceCreateSpec.validate_care_typecallsDeviceTypeRegistry.get_care_device_class(value), which raises for unknown types.care_typecan't change on update —DeviceUpdateSpecdoesn't expose it.DeviceServiceHistory.serviced_onandnoteare required on write; updates are blocked onceedit_historyreaches 50 entries ("Cannot Edit instance anymore").ContactPoint.system,value, anduseare all required for each contact entry.
Methods & save behaviour
Device.save() — organization cache
Every save rebuilds facility_organization_cache:
- Look up the
FacilityOrganizationwithorg_type="root"for the device'sfacility; its ID seeds the cache. - If
managing_organizationis set, add itsidand itsparent_cache(the full ancestor chain). - 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:
| Stage | Hook | Effect |
|---|---|---|
| create | handle_create(request_data, obj) | May mutate obj / obj.metadata and persist (e.g. the bundled camera plugin stores request_data["some_data"] into metadata) |
| update | handle_update(request_data, obj) | Same, on update |
| destroy | handle_delete(obj) | Validation/cleanup before the device is soft-deleted (deleted=True) |
| list | list(obj) | Supplies care_metadata for DeviceListSpec |
| retrieve | retrieve(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
facilitycomes from the URL (facility_external_id) on create, not from the body.current_location,current_encounter, andmanaging_organizationaren't writable through the device create/update body. Dedicated detail actions mutate them, and each one closes the open history row (stampsend) before opening a new one:POST associate_encounter—{ encounter: uuid | null }; closes the openDeviceEncounterHistory, setscurrent_encounter, and opens (and returns) a new history row.nulldetaches.POST associate_location—{ location: uuid | null }; same pattern againstDeviceLocationHistory.POST add_managing_organization—{ managing_organization: uuid }.POST remove_managing_organization— clearsmanaging_organization.
- When an encounter reaches a completed status,
disassociate_device_from_encounterclearscurrent_encounteron all attached devices and stampsendon their openDeviceEncounterHistoryrows. metadata(the model column) is plugin-owned; clients read deployment-specific data back through thecare_metadatavirtual field, notmetadata.facility_organization_cacheis 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_cachewith?include_children=.
Related
- Reference: Facility
- Reference: Organization
- Reference: Location
- Reference: Encounter
- Source: device.py model
- Source: device spec
- Source: device history spec
- Source: ContactPoint