|
| 1 | +# presence-floorplan Specification |
| 2 | + |
| 3 | +## Purpose |
| 4 | +TBD - created by archiving change floorplan-presence-sensors. Update Purpose after archive. |
| 5 | +## Requirements |
| 6 | +### Requirement: Reuse of existing Floorplan component |
| 7 | + |
| 8 | +The presence-sensor visualisation SHALL be implemented by extending `src/components/floorplan/Floorplan.vue`, not by introducing a new floorplan component. No tabs, no floor selector, and no new layout type SHALL be added; floor switching continues to work via the existing `$store.state.gui.floorplan` mechanism that all `Kiosk*` views use. |
| 9 | + |
| 10 | +#### Scenario: No new floorplan component |
| 11 | + |
| 12 | +- **WHEN** the change is implemented |
| 13 | +- **THEN** there is no new file under `src/components/floorplan/` that re-implements the floorplan rendering |
| 14 | +- **AND** the presence-point rendering lives inside `Floorplan.vue` |
| 15 | + |
| 16 | +#### Scenario: No tabs in presence view |
| 17 | + |
| 18 | +- **WHEN** `KioskPresence.vue` is rendered |
| 19 | +- **THEN** it contains no tab/segment control for selecting floors |
| 20 | + |
| 21 | +### Requirement: KioskPresence view mirrors KioskLights structure |
| 22 | + |
| 23 | +`src/views/KioskPresence.vue` SHALL be a structural 1:1 clone of `src/views/KioskLights.vue`: same `<v-container>` wrapper, same `<Floorplan>` instantiation pattern with colour props, same `KioskLinkPanel` back-link to `/app/kioskoverview`, same `KioskTogglePanel` controlling `displayEnhancedDialog`, and the same `mounted()` call to `kioskMode(true)`. |
| 24 | + |
| 25 | +#### Scenario: Filter prop |
| 26 | + |
| 27 | +- **WHEN** `KioskPresence.vue` instantiates `<Floorplan>` |
| 28 | +- **THEN** it passes `:applianceTypeFilter="['OCCUPANCY_SENSOR']"` |
| 29 | +- **AND** does not pass a `classFqnFilter` |
| 30 | + |
| 31 | +#### Scenario: Layout parity with Lights |
| 32 | + |
| 33 | +- **WHEN** `KioskPresence.vue` is compared structurally to `KioskLights.vue` |
| 34 | +- **THEN** the template tree (root `<div class="home">`, `<v-container>`, child component order) matches; only the `applianceTypeFilter`, the `icon` prop, and the i18n title differ |
| 35 | + |
| 36 | +### Requirement: Routing and navigation |
| 37 | + |
| 38 | +A new lazy-loaded route `/app/kioskpresence` named `kioskPresence` SHALL be added to `src/router/index.ts`, and the navigation drawer SHALL link to it. |
| 39 | + |
| 40 | +#### Scenario: Route is registered |
| 41 | + |
| 42 | +- **WHEN** the user navigates to `/app/kioskpresence` |
| 43 | +- **THEN** the router renders `KioskPresence.vue` |
| 44 | + |
| 45 | +#### Scenario: Drawer entry exists |
| 46 | + |
| 47 | +- **WHEN** the navigation drawer is opened |
| 48 | +- **THEN** an entry labelled with `page.kiosk.presence.title` links to `/app/kioskpresence` |
| 49 | + |
| 50 | +### Requirement: Live target points rendered by Floorplan |
| 51 | + |
| 52 | +For every appliance whose `usageType === 'OCCUPANCY_SENSOR'` is placed on the active floor, `Floorplan.vue` SHALL render one absolutely-positioned point overlay per entry in `state.presences[0].objects[]`, transformed from sensor-local metres into plan pixels using the appliance's `iconPos` and the per-appliance `{ yawDeg, pxPerMeter }` from `src/lib/presence/sensorOrientation.ts`. |
| 53 | + |
| 54 | +#### Scenario: Single target |
| 55 | + |
| 56 | +- **WHEN** the SSE stream reports `objects: [{ id: 1, x: 1.0, y: 2.0 }]` for a sensor whose `iconPos = [100, 200]` and orientation entry is `{ yawDeg: 0, pxPerMeter: 40 }` |
| 57 | +- **THEN** exactly one point overlay is rendered at plan-pixel coordinates derived from `(100 + 1.0·40, 200 + 2.0·40)` (with the project's plan-Y orientation applied consistently with `iconPos`), scaled by the floorplan's `scale` factor |
| 58 | + |
| 59 | +#### Scenario: Multiple targets |
| 60 | + |
| 61 | +- **WHEN** `objects` contains three entries |
| 62 | +- **THEN** three point overlays are rendered, one per entry |
| 63 | + |
| 64 | +#### Scenario: Target leaves |
| 65 | + |
| 66 | +- **WHEN** an `objects` entry from the previous frame is no longer present in the next frame |
| 67 | +- **THEN** its point overlay disappears within one render cycle |
| 68 | + |
| 69 | +#### Scenario: Cross-vendor parity |
| 70 | + |
| 71 | +- **WHEN** Shelly Presence Gen4 and Apollo MTR-1 appliances both have placed entries |
| 72 | +- **THEN** their target points are rendered through the same code path, with no vendor-specific branching beyond reading the same `presences[0].objects[]` paths |
| 73 | + |
| 74 | +#### Scenario: Z fields are ignored |
| 75 | + |
| 76 | +- **WHEN** a Shelly target carries non-null `z`, `minZ`, `maxZ` |
| 77 | +- **THEN** those fields do NOT influence the rendered point |
| 78 | + |
| 79 | +### Requirement: Footprint comes from existing imgMapCoords polygon |
| 80 | + |
| 81 | +The presence-sensor footprint on the plan SHALL be the existing `imgMapCoords` polygon that `Floorplan.vue` already draws for every placed appliance. No new coverage-cone polygon SHALL be computed or rendered. |
| 82 | + |
| 83 | +#### Scenario: No coverage-cone module |
| 84 | + |
| 85 | +- **WHEN** the change is implemented |
| 86 | +- **THEN** there is no module computing footprint polygons from `(ceilingHeight, hAngle, vAngle, range)` |
| 87 | +- **AND** ceiling-height constants are not introduced anywhere in the frontend |
| 88 | + |
| 89 | +#### Scenario: Admin-drawn polygon is the footprint |
| 90 | + |
| 91 | +- **WHEN** an `OCCUPANCY_SENSOR` appliance is placed with `imgMapCoords` |
| 92 | +- **THEN** that polygon is the footprint shown in the view, identically to how Floorplan handles all other sensor types |
| 93 | + |
| 94 | +### Requirement: SSE subscription via pathsForApplianceType |
| 95 | + |
| 96 | +`src/utils/overmindUtils.ts` SHALL declare presence subscription paths for the new `OCCUPANCY_SENSOR` type so that `Floorplan.vue`'s existing transport-registration loop subscribes automatically without any presence-specific code in `Floorplan.vue`'s mount logic. |
| 97 | + |
| 98 | +#### Scenario: COMPACT_PATHS entry |
| 99 | + |
| 100 | +- **WHEN** `pathsForApplianceType('OCCUPANCY_SENSOR', 'compact')` is called |
| 101 | +- **THEN** it returns `['presences[0].presence', 'presences[0].objects']` |
| 102 | + |
| 103 | +#### Scenario: DETAIL_PATHS entry |
| 104 | + |
| 105 | +- **WHEN** `pathsForApplianceType('OCCUPANCY_SENSOR', 'detail')` is called |
| 106 | +- **THEN** it returns `['**']` |
| 107 | + |
| 108 | +#### Scenario: ApplianceType union |
| 109 | + |
| 110 | +- **WHEN** the `ApplianceType` type alias is inspected |
| 111 | +- **THEN** it includes the literal `'OCCUPANCY_SENSOR'` |
| 112 | + |
| 113 | +### Requirement: Reactive writes via setPathValue |
| 114 | + |
| 115 | +The transport-update path-walk MUST continue to use `setPathValue` from `overmindUtils.ts` (which uses `Vue.set` for every dynamic hop) so that a freshly tracked target — i.e. a previously unseen index in `presences[0].objects` — is reactive without any additional code. |
| 116 | + |
| 117 | +#### Scenario: New target appears |
| 118 | + |
| 119 | +- **WHEN** a `transport-update` adds a previously unseen index to `presences[0].objects` for an `OCCUPANCY_SENSOR` appliance |
| 120 | +- **THEN** the new entry is reactive and triggers a re-render of the point layer without a manual `Vue.set` call in `Floorplan.vue` |
| 121 | + |
| 122 | +### Requirement: Sensor orientation map in frontend |
| 123 | + |
| 124 | +`src/lib/presence/sensorOrientation.ts` SHALL export a `Record<number, { yawDeg: number; pxPerMeter: number }>` keyed by appliance id, plus a `DEFAULT_ORIENTATION` constant. When an `OCCUPANCY_SENSOR` appliance has no entry, the default SHALL be used and a console warning SHALL be emitted (once per appliance id per session). |
| 125 | + |
| 126 | +#### Scenario: Known appliance |
| 127 | + |
| 128 | +- **WHEN** appliance id 206 has an entry `{ yawDeg: 90, pxPerMeter: 50 }` |
| 129 | +- **THEN** that orientation is used for projecting its targets |
| 130 | + |
| 131 | +#### Scenario: Unknown appliance falls back to default |
| 132 | + |
| 133 | +- **WHEN** appliance id 999 (`OCCUPANCY_SENSOR`) has no entry in `sensorOrientation.ts` |
| 134 | +- **THEN** `DEFAULT_ORIENTATION` is used |
| 135 | +- **AND** a console warning is emitted exactly once per session for that id |
| 136 | + |
| 137 | +### Requirement: Read-only |
| 138 | + |
| 139 | +The view and the presence overlay MUST NOT issue any write operations against `OCCUPANCY_SENSOR` appliances (no `POST /execute`, no config writes). |
| 140 | + |
| 141 | +#### Scenario: No execute calls |
| 142 | + |
| 143 | +- **WHEN** the user interacts with the view (clicks, taps, opens dialogs) |
| 144 | +- **THEN** no `POST /execute` request is sent to any `OCCUPANCY_SENSOR` appliance |
| 145 | + |
0 commit comments