# GIMA (GIMAHub) Integration

## Purpose

Pluscare receives health data from GIMA medical devices (smartwatches, thermometers, blood-pressure monitors, glucometers, etc.). Unlike Withings — where each account maps to one person — GIMA devices talk to a **gateway** (a USB dongle / smartbox), and the gateway relays their measures to Pluscare over a webhook.

A single gateway can be shared: one dongle installed at home or in a hospital ward, with **several devices (e.g. several watches) belonging to different patients** all reporting through it. This feature routes every measure to the **patient that owns the measuring device**, so shared-gateway setups never mix up whose data is whose.

## How it works

1. Devices communicate with a gateway over BLE; the gateway pushes measures to Pluscare via webhook.
2. Each webhook payload identifies **both** the gateway (`gateway.id`) and the measuring device (`device.id`), plus the list of measures.
3. Pluscare resolves the patient from the **measuring device serial** (`device.id`), not from the gateway.

### Per-device patient routing

The routing key is the measuring device, modeled as a `Device` row with `serial_number = device.id`, `manufacturer = Gima`. The patient is `Device.user`.

- **Device assigned to a patient** → measures are stored against that patient; each metric row records the measuring device in `device_id`.
- **Device unknown** (first time it is seen) → it is **auto-registered** as an *available* device (`user_id = null`) under the gateway's company, capturing model / battery / charging. It then appears in the device list for an operator to assign.
- **Device known but not yet assigned** → **strict routing**: measures are **not stored** (no misattribution). The device shows an *Unassigned* badge until an operator assigns it. This is the signal that a device may be transmitting data that is currently going nowhere.
- **Legacy payload without a `device.id`** → falls back to the gateway's own patient (the historical one-gateway-one-patient model).

The gateway itself stays a `Device` row (`type = gateway`) but is treated as shared infrastructure: it must be provisioned once by an operator (so the auto-registered devices inherit its company) and does not need a patient.

### Battery

`device.battery` (percentage) and `device.charging` are captured on every measure and shown in the UI (device list column + patient device card). Gateways run on mains power and do not report a battery level.

### Managing devices & assignments (admin device page)

The `/devices` page (super-admin only; company admins see a read-only view) lets an operator:

- see the **patient** each device is associated with (or an amber **"Unassigned"** warning badge);
- **filter** by free-text search, **status** (all / assigned / unassigned), **manufacturer**, **type**, and **patient** ("show me every device this patient has");
- **assign / unassign** a patient directly from the device's edit panel (searchable patient picker scoped to the device's company).

Assignment is validated server-side: the target must be a **patient** and belong to the **same company** as the device. (Devices can still be assigned from the patient's own edit form as before — both paths write `devices.user_id`.)

## Key entities & DB

| Entity | Notes |
|--------|-------|
| `devices.user_id` | The patient that owns the device (one device → at most one patient). Source of truth for routing and for "which devices does a patient have". |
| `devices.serial_number` | For GIMA measuring devices this is the payload `device.id`; for gateways it is `gateway.id`. |
| `devices.gateway_serial` | *(new)* Diagnostic: serial of the gateway that last relayed measures for this device. |
| `devices.battery` / `devices.charging` | *(new)* Last reported battery percentage and charging state. |
| `devices.type` | `watch` \| `balance` \| `thermometer` \| `blood_pressure` \| `oximeter` \| `ecg` \| `glucometer` \| `spirometer` \| `lab_analyzer` \| `gateway`. On auto-registration it is inferred from the payload's `device.measure_types` (see below). |
| metric tables `.device_id` | The **measuring** device that produced the reading (previously stored the gateway id). |
| `gimahub_webhook_events` | Raw webhook envelope (idempotency key, payload, processed_at, error) for auditing/replay. |

### Example measurements payload (trimmed)

```json
{
  "gateway": { "id": "bc572915a3a3", "model": "KG04" },
  "device":  { "id": "cf05e524d5ef", "manufacturer": "DOMETHICS", "model": "S1MPL0", "battery": 32, "charging": false },
  "measures": [
    { "type": "blood_pressure_periodic", "timestamp": "20260722124017", "systolic": 110, "diastolic": 71, "pulse_rate": 51 },
    { "type": "temperature_periodic",    "timestamp": "20260722124017", "temperature": 36.7 }
  ]
}
```

## Technical flow

1. `GimaHubWebhookController` stores the raw event (idempotent) and dispatches `ProcessGimaHubWebhookEvent`.
2. The job looks up the gateway by `gateway.id`; if absent it drops the event.
3. `resolvePatient()` resolves the measuring device via `DeviceRegistrar::resolveOrRegister()` (auto-registering unknown devices, refreshing battery / gateway / company), then returns the device's patient — or `null` when the device is unassigned (strict skip).
4. Per-type handlers store each measure against the patient, tagging it with the measuring `device_id`; alert metrics are then re-evaluated.
5. The gateway's (and device's) `sync_at` is bumped so the sync-alert job can detect silent devices.

## Notes / edge cases

- **Generic by design**: patient resolution goes through a vendor-agnostic `DeviceRegistrar` (serial → patient). GIMA uses it today; other push-based integrations can reuse it.
- **Withings** has no shared gateway — each account is one person and routing is by OAuth token → user, so the multi-patient collision cannot happen there. No change was needed for Withings.
- **Type inference**: the payload's device `type` is a model code (e.g. `S1MPL0`), not our category, so the device type is derived from `device.measure_types` when auto-registering. Mapping (first match wins, wearable markers take precedence because multi-sensor watches also report temperature/spo2/bp): `steps_periodic`/`activity`/`sleep` → `watch`; `body_composition` → `balance`; `blood_pressure*` → `blood_pressure`; `ecg_2lead` → `ecg`; `spo2*` → `oximeter`; `temperature*` → `thermometer`; `spirometry*` → `spirometer`; `glucose` → `glucometer`; `hemoglobin`/`ketone`/`cholesterol`/`lactate`/`uric_acid`/`triglycerides` → `lab_analyzer`. Falls back to `watch` when nothing matches; an operator can always correct it. The inferred type only pre-fills a **new** device — it never overwrites an existing one.
- **Patient search** for assignment/filtering is served by `GET /patients/search` (`search-patients`), scoped by visibility and optionally by company.
- **Logging**: high-volume metric traces (webhook payloads, per-measure notes, and the Withings API call/response dumps) go through `App\Support\MetricLog::local()` and are emitted **only in the local environment** (`APP_ENV=local`), so production logs are not flooded. Actionable warnings/errors keep using `Log::warning()` / `Log::error()` and are always logged.

## Related documentation

- [Withings Integration](./withings-integration.md) — the OAuth/account-based counterpart
- [Metric Alerts](./metric-alerts.md) — how stored measures trigger alerts
- [Business Logic](../business-logic.md) — devices, patients, companies
