Properties¶
Properties are configuration values that define how a device behaves.
What is a Property?¶
+------------------+ +------------------+ +------------------+
| PQ System | ---> | Adapter | ---> | Physical Device |
+------------------+ +------------------+ +------------------+
Admin sets: Stores locally Applies config:
"unlock_duration: 5" Sends to device door stays
unlocked for 5s
Properties are:
- Configuration - set by administrator, not by device
- Persistent - stored in PQ database
- Rarely changed - unlike status which changes constantly
- Applied to device - adapter sends to device when changed
Property vs Status vs Event¶
| Aspect | Property | Status | Event |
|---|---|---|---|
| Who sets | Admin/User | Device | Device |
| Direction | PQ -> Device | Device -> PQ | Device -> PQ |
| Frequency | Rarely changes | Changes often | One-time occurrence |
| Example | "unlock duration: 5s" | "door is open" | "door was opened" |
PROPERTY: "Door unlock duration = 5 seconds"
(configured by admin, applied to device)
STATUS: "Door is currently unlocked"
(reported by device, changes over time)
EVENT: "Door was unlocked at 10:15"
(happened once, recorded in history)
Common Property Types¶
Device Properties¶
| Property | Type | Description |
|---|---|---|
name |
string | Display name |
location |
string | Physical location |
ip_address |
string | Network address |
port |
int | Communication port |
Door Properties¶
| Property | Type | Description |
|---|---|---|
unlock_duration |
int | Seconds door stays unlocked |
held_open_timeout |
int | Seconds before "door held" alarm |
force_open_alarm |
bool | Trigger alarm on forced entry |
relock_on_close |
bool | Lock immediately when door closes |
Reader Properties¶
| Property | Type | Description |
|---|---|---|
led_mode |
enum | LED behavior |
beep_on_read |
bool | Sound on card read |
direction |
enum | Entry/Exit/Both |
Property Definition in YAML¶
Properties are defined in adapter-registration.yaml:
device_types:
- type_id: Door
properties:
- name: unlock_duration
type: int
default: 5
description: Seconds door stays unlocked after valid access
range: [1, 60]
- name: held_open_timeout
type: int
default: 30
description: Seconds before door-held alarm triggers
range: [0, 300]
- name: force_open_alarm
type: bool
default: true
description: Generate alarm when door forced open
- name: relock_on_close
type: bool
default: false
description: Automatically lock when door closes
Property Flow¶
Admin changes "unlock_duration" to 10s
|
v
+----------------------------------+
| PQ System |
| Stores property in database |
| Sends SetProperty command |
+----------------------------------+
|
v
[NATS message]
|
v
+----------------------------------+
| Adapter |
| Receives SetProperty command |
| Updates local cache |
| Sends to device via protocol |
+----------------------------------+
|
v
+----------------------------------+
| Physical Device |
| Applies new configuration |
| Door now unlocks for 10s |
+----------------------------------+
Implementing Property Handling¶
public class DoorController : Thing
{
// property backing fields with defaults
private int _unlockDuration = 5;
private int _heldOpenTimeout = 30;
private bool _forceOpenAlarm = true;
// property accessors
public int UnlockDuration
{
get => _unlockDuration;
set
{
if (value == _unlockDuration) return;
_unlockDuration = value;
ApplyToDevice(); // send to physical device
}
}
private async void ApplyToDevice()
{
await _protocol.SetDoorConfig(DeviceId, new DoorConfig
{
UnlockDuration = _unlockDuration,
HeldOpenTimeout = _heldOpenTimeout,
ForceOpenAlarm = _forceOpenAlarm
});
}
}
Property Sync on Startup¶
When adapter starts, properties need to sync:
Adapter starts
|
v
+------------------+
| Fetch properties | <-- from PQ database
| from PQ API |
+------------------+
|
v
+------------------+
| Apply to local |
| Thing objects |
+------------------+
|
v
+------------------+
| Send to devices | <-- via protocol
| (if online) |
+------------------+
public override async Task Start(CancellationToken ct)
{
// 1. fetch stored properties
var properties = await _pqApi.GetDeviceProperties(_adapterId, ct);
// 2. apply to things
foreach (var (deviceId, props) in properties)
{
var thing = FindThing(deviceId);
thing.ApplyProperties(props);
}
// 3. sync to devices
foreach (var device in GetOnlineDevices())
{
await SyncPropertiesToDevice(device, ct);
}
}
Property Types¶
| YAML Type | C# Type | Example |
|---|---|---|
string |
string |
"Main Entrance" |
int |
int |
5 |
bool |
bool |
true |
enum |
enum |
Direction.Entry |
float |
float |
0.75 |
guid |
Guid |
"a1b2c3d4-..." |
TypeName |
TypeName? |
Device reference (resolved by GUID) |
TypeName[] |
IReadOnlyList<TypeName> |
Array of device references |
Device Reference Properties¶
When property type matches a type_id in your adapter, it becomes a device reference:
properties:
target_door:
type: Door # single reference (nullable)
partitions:
type: Partition[] # array reference (many-to-many)
See Things - Device References for details.
Enum Properties¶
public enum ReaderDirection { Entry, Exit, Both }
public ReaderDirection Direction { get; set; } = ReaderDirection.Both;
Sensitive Properties¶
Mark passwords, API keys, and other secrets with sensitive: true:
properties:
password:
type: string
required: false
sensitive: true
category: connection
description: Authentication password
sensitive: true does two things:
- Masks the UI field - the configuration input renders as a password field (dots instead of text) rather than plain text.
- Masks the audit trail - property-change records store a masked placeholder, so the secret never lands in audit history.
Not encrypted at rest
sensitive only masks display. The value is stored in the PQ database in
plaintext - anyone with database access or device-property API access can
read it. Do not treat sensitive as at-rest protection.
Property Validation¶
Validate before applying:
public int UnlockDuration
{
get => _unlockDuration;
set
{
// validate range
if (value < 1 || value > 60)
throw new ArgumentOutOfRangeException(
nameof(value),
"Unlock duration must be 1-60 seconds");
_unlockDuration = value;
ApplyToDevice();
}
}
Properties vs Hardcoded Values¶
// WRONG - hardcoded, cannot be changed
await _protocol.UnlockDoor(deviceId, duration: 5);
// RIGHT - use property
await _protocol.UnlockDoor(deviceId, duration: door.UnlockDuration);
If a value might need to change per-device, make it a property.
Common Mistakes¶
1. Not Syncing on Startup¶
// WRONG - uses defaults even when PQ has different values
public override Task Start(CancellationToken ct)
{
await _protocol.Connect(ct);
// forgot to fetch and apply stored properties!
}
2. Not Validating¶
// WRONG - accepts any value
public int UnlockDuration { get; set; }
// device might not support duration > 60s, will fail silently
3. Mixing Properties and Status¶
// WRONG - "isLocked" is status, not property
properties:
- name: isLocked
type: bool
// RIGHT - "isLocked" is status (device reports it)
// Property would be "defaultLockState" (what admin wants)
Properties in Device Hierarchy¶
Properties can exist at different levels:
Adapter
|-- default_timeout: 30 <-- adapter-level default
|
+-- Door Controller
|-- communication_retry: 3
|
+-- Door 1
| |-- unlock_duration: 5
| +-- held_open_timeout: 30
|
+-- Door 2
|-- unlock_duration: 10 <-- different value
+-- held_open_timeout: 60
Child can inherit from parent or override with specific value.
Next: Things - Device hierarchy