public final class Trait
- Object
- Trait
One capability an accessory can have: a light’s brightness, a lock’s state,
a sensor’s reading. Instances are interned constants, so == is a valid
identity test and is used throughout the API.
A trait is canonical, not a platform identifier. BRIGHTNESS is the
same constant whether the accessory is behind HomeKit or Matter, and the
port maps it to HMCharacteristicTypeBrightness or to Level Control’s
CurrentLevel. No HMCharacteristicType string and no Matter cluster id
ever reaches Java; the mapping tables live in the native bridges, where the
platform types are, and a test parses both of them to make sure neither has
drifted from this list.
Read the constant before you use it
The javadoc on each constant below is the specification, and most of them carry a caveat that matters. Three kinds recur:
Polarity. Some values run in opposite directions on the two backends.
COVERING_POSITION is the worst: HomeKit’s 100 is fully open and Matter’s
0 is. This API picks one convention, the port inverts, and the constant
says which way it runs. Do not assume.
Range and quantization. Everything proportional is normalized to
percent here, but Matter carries brightness as 0 to 254 and covering
position as 0 to 10000, so a round trip is not the identity. Where an
accessory declares a real range or step, it is on the
TraitConstraint, not here.
Absence. Several traits genuinely do not exist on one backend –
OUTLET_IN_USE and TARGET_HUMIDITY have no Matter equivalent at all.
Those report HomeError.TRAIT_NOT_SUPPORTED rather than a fabricated
value. The constant says so.
Why this is a class and not an enum
The same reason com.codename1.health.HealthDataType is: the table grows,
each constant carries metadata, and forId(java.lang.String) has to stay
total across versions so a port from a newer build naming a trait this one
does not have degrades to “unknown trait” rather than throwing.
There is deliberately no way to make a custom trait. A trait only means something because both ports agree on how to map it; one this API does not know is one no port can serve.
Fields
Methods
public String getId() | The token this trait crosses the native boundary as, and the stable name to use in build hints and persisted state. |
public TraitValueKind getValueKind() | What sort of value this trait carries, and therefore which TraitValue getter reads it. |
public TraitUnit getUnit() | The unit a TraitValueKind.DOUBLE trait is expressed in. |
public boolean acceptsUnit(TraitUnit unit) | Whether this trait can only ever be read. |
public TraitValue enumValue(int ordinal) | Whether a value belongs to this trait’s domain enum. |
public boolean acceptsEnumValue(TraitValue value) | |
public boolean acceptsEnumWrite(TraitValue value) | Whether a value may be WRITTEN to this trait. |
public boolean isReadOnly() | A trait that is writable in principle may still be read-only on a particular accessory; that is a property of the TraitConstraint, not of the trait. |
public boolean hasNominalRange() | Whether getNominalMinimum() and getNominalMaximum() mean anything. |
public double getNominalMinimum() | The bottom of this trait’s documented range, in getUnit(). |
public double getNominalMaximum() | The top of this trait’s documented range, in getUnit(). |
public static List<Trait> all() | Every trait this release knows, in declaration order. |
public static Trait forId(String id) | Resolves a trait by its getId(), total: an unknown or null id answers null rather than throwing. |
public String toString() | Returns a string representation of the object. |
Inherited methods
Field details
ON_OFF
public static final Trait ON_OFFWhether the accessory is on.
HomeKit spells this two ways – PowerState on a lightbulb or switch,
and Active on a fan v2, air purifier or heater-cooler – and this one
constant covers both; the port uses whichever characteristic the
service actually has. Two traits here would be a HomeKit implementation
detail leaking into portable code.
Matter: On/Off cluster 0x0006, OnOff attribute.
Note that on Matter this is genuinely independent of
BRIGHTNESS: level 0 does not reliably mean off, and turning a light
off does not zero its level. Write this trait to turn something off.
OUTLET_IN_USE
public static final Trait OUTLET_IN_USEWhether something is plugged into an outlet and drawing power.
HomeKit: OutletInUse.
Absent on Matter and Google Home. Matter’s Electrical Power
Measurement cluster answers a different question – how much current is
flowing – and it is optional and rare besides. A lamp that is plugged
in but switched off draws nothing, so deriving this from power draw
would report an empty socket. The port answers
HomeError.TRAIT_NOT_SUPPORTED rather than guessing.
BRIGHTNESS
public static final Trait BRIGHTNESSHow bright a light is, as a percentage.
HomeKit: Brightness, already a percentage.
Matter: Level Control 0x0008, CurrentLevel, which runs 1 to 254
with the real bounds in MinLevel and MaxLevel; written through
MoveToLevel. 254 steps do not divide into 100, so setting 33
percent and reading it back can answer 33 or something adjacent. Do not
compare a read against a value you wrote.
HUE
public static final Trait HUEThe hue of a colour light, in degrees around the colour wheel.
HomeKit: Hue, already in arcdegrees.
Matter: Color Control 0x0300, CurrentHue – a single byte covering
the whole circle, so about 1.4 degrees per step. Where the accessory
supports the enhanced-hue feature the port reads
EnhancedCurrentHue instead and the resolution is much better.
A Matter light in XY or colour-temperature mode reports a stale hue.
The port checks ColorMode and reports no value rather than passing on
a number that does not describe the light you are looking at.
SATURATION
public static final Trait SATURATIONHow saturated a colour light is, as a percentage.
HomeKit: Saturation. Matter: Color Control 0x0300,
CurrentSaturation, 0 to 254. Same quantization and the same
colour-mode caveat as HUE.
COLOR_TEMPERATURE
public static final Trait COLOR_TEMPERATUREThe colour temperature of a white light, in mireds.
HomeKit: ColorTemperature. Matter: Color Control 0x0300,
ColorTemperatureMireds, bounded by the accessory’s own
ColorTempPhysicalMinMireds and ColorTempPhysicalMaxMireds, which
arrive on the TraitConstraint.
Mireds because that is what both platforms use natively, so nothing
is converted on the way through. And the trap that catches everyone
once: a higher mired value is a warmer light. 153 is a cold
blue-white and 400 is candlelight. Use
TraitValue.getColorTemperatureKelvin() if you would rather think in
Kelvin.
CURRENT_LIGHT_LEVEL
public static final Trait CURRENT_LIGHT_LEVELAmbient light, in lux.
HomeKit: CurrentLightLevel, already in lux.
Matter: Illuminance Measurement 0x0400, MeasuredValue, which is
logarithmically encoded as 10000 * log10(lux) + 1 in a single
16-bit field – so precision falls away sharply at the bright end. Its
two sentinels, 0 for “too dark to measure” and 0xFFFF for “unknown”,
become no value rather than a reading of zero lux.
CURRENT_TEMPERATURE
public static final Trait CURRENT_TEMPERATUREThe temperature an accessory is measuring.
HomeKit: CurrentTemperature on a sensor, or the thermostat’s own.
Matter: Temperature Measurement 0x0402 MeasuredValue for a sensor,
or Thermostat 0x0201 LocalTemperature for a thermostat. Both are
hundredths of a degree with 0x8000 meaning null, which becomes no
value.
TARGET_TEMPERATURE
public static final Trait TARGET_TEMPERATUREThe single setpoint that applies in the thermostat’s current mode.
The largest structural mismatch in this table
HomeKit has one TargetTemperature characteristic. Matter has no
such thing – it has OccupiedHeatingSetpoint and
OccupiedCoolingSetpoint and nothing that unifies them.
So this trait is defined as the setpoint that applies right now: when
TARGET_HEATING_COOLING is HeatingCoolingMode.HEAT it is the
heating setpoint, when it is HeatingCoolingMode.COOL it is the
cooling one, and when the mode is HeatingCoolingMode.AUTO this
trait reports no value on either backend. In auto there are two
setpoints and answering with either one would silently be the wrong one
half the time; use TARGET_HEATING_TEMPERATURE and
TARGET_COOLING_TEMPERATURE, which always mean exactly what they say.
A thermostat UI that only ever offers one number will be wrong on an auto-mode thermostat no matter what this API does. Offer two.
TARGET_HEATING_TEMPERATURE
public static final Trait TARGET_HEATING_TEMPERATUREThe temperature below which heating runs.
HomeKit: HeatingThreshold. Matter: Thermostat 0x0201,
OccupiedHeatingSetpoint. Clean on both sides.
TARGET_COOLING_TEMPERATURE
public static final Trait TARGET_COOLING_TEMPERATUREThe temperature above which cooling runs.
HomeKit: CoolingThreshold. Matter: Thermostat 0x0201,
OccupiedCoolingSetpoint. Clean on both sides.
CURRENT_HEATING_COOLING
public static final Trait CURRENT_HEATING_COOLINGWhat the thermostat is doing right now, as a HeatingCoolingMode.
HomeKit: CurrentHeatingCooling. Matter: Thermostat 0x0201,
ThermostatRunningMode where the accessory has it, falling back to
SystemMode.
Never HeatingCoolingMode.AUTO: at any instant a thermostat is
heating, cooling or idle. Auto is a policy, and it lives on
TARGET_HEATING_COOLING.
TARGET_HEATING_COOLING
public static final Trait TARGET_HEATING_COOLINGWhat the thermostat has been asked to do, as a HeatingCoolingMode.
HomeKit: TargetHeatingCooling, four values. Matter: Thermostat
0x0201, SystemMode, nine.
The five Matter modes HomeKit cannot express – emergency heat,
precooling, fan-only, dry and sleep – arrive as
HeatingCoolingMode.OTHER with the platform’s own ordinal on
TraitValue.getRawPlatformValue(), rather than being flattened into
HeatingCoolingMode.OFF. Writing OTHER is refused.
CURRENT_HUMIDITY
public static final Trait CURRENT_HUMIDITYThe relative humidity an accessory is measuring, as a percentage.
HomeKit: CurrentRelativeHumidity. Matter: Relative Humidity
Measurement 0x0405, MeasuredValue, in hundredths of a percent with
0xFFFF meaning null.
TARGET_HUMIDITY
public static final Trait TARGET_HUMIDITYThe relative humidity a humidifier or dehumidifier is aiming for.
HomeKit: TargetRelativeHumidity.
Absent on Matter and Google Home. There is no standard
target-humidity cluster in the Matter revisions the shipping ecosystems
support, so the port answers HomeError.TRAIT_NOT_SUPPORTED.
LOCK_STATE
public static final Trait LOCK_STATEWhat a door lock is doing, as a LockState.
HomeKit: CurrentLockMechanismState. Matter: Door Lock 0x0101,
LockState, which is nullable and whose null becomes no value.
Note LockState.JAMMED is unreachable outside HomeKit and
LockState.PARTIALLY_LOCKED is unreachable on HomeKit; the enum
explains both.
TARGET_LOCK_STATE
public static final Trait TARGET_LOCK_STATELock or unlock a door lock. Only LockState.SECURED and
LockState.UNSECURED may be written.
HomeKit: TargetLockMechanismState, an ordinary characteristic write.
Matter has no such attribute – locking is the LockDoor command
and unlocking is UnlockDoor on Door Lock 0x0101. That difference is
invisible from here by design, with one exception that is not: a Matter
lock configured with RequirePINforRemoteOperation will refuse an
unlock without a credential and the write fails with
HomeError.PIN_REQUIRED. Supply one through
TraitWrite.setAuthorizationData(java.lang.String). HomeKit never
takes a PIN and ignores the field.
DOOR_STATE
public static final Trait DOOR_STATEWhat a door or garage door is doing, as a DoorState.
HomeKit: CurrentDoorState. Google Home: its own garage-door trait.
Matter has no standard garage cluster, so this is unavailable on an
Android build limited to HomeAvailability.COMMISSIONING_ONLY.
TARGET_DOOR_STATE
public static final Trait TARGET_DOOR_STATEOpen or close a door. Only DoorState.OPEN and DoorState.CLOSED may
be written.
HomeKit: TargetDoorState. Same backend availability as
DOOR_STATE.
OBSTRUCTION_DETECTED
public static final Trait OBSTRUCTION_DETECTEDWhether something is blocking a door or covering.
HomeKit: ObstructionDetected, on both doors and coverings.
Matter: the obstacle bit of Window Covering 0x0102 SafetyStatus –
coverings only, since there is no garage cluster to carry it.
COVERING_POSITION
public static final Trait COVERING_POSITIONHow far open a blind or shade is. 100 is fully open, 0 is fully closed.
The inversion to watch
This is the likeliest source of a shipped bug in the whole package.
HomeKit’s CurrentPosition runs the way this constant does – 100 is
open. Matter’s CurrentPositionLiftPercent100ths runs the other
way, where 0 is fully open, and it is in hundredths of a percent
besides, so the raw numbers differ by both a flip and a factor of a
hundred.
This API takes the HomeKit convention because it is what a slider labelled “open” wants, and the Android bridge inverts. Nothing in application code should ever see Matter’s polarity – but if you are reading a Matter trace next to a Codename One log, that is why the two disagree.
TARGET_COVERING_POSITION
public static final Trait TARGET_COVERING_POSITIONMove a blind or shade. 100 is fully open, matching
COVERING_POSITION; read that constant’s note.
HomeKit: TargetPosition. Matter: TargetPositionLiftPercent100ths
to read back what was asked for, and the GoToLiftPercentage command
to write.
COVERING_TILT
public static final Trait COVERING_TILTHow far the slats of a blind are tilted, as a percentage.
HomeKit models tilt as two characteristics in arcdegrees,
CurrentHorizontalTilt and CurrentVerticalTilt, running -90 to 90.
Matter models it as one percentage,
CurrentPositionTiltPercent100ths.
This API takes Matter’s single-axis percentage. On iOS the port uses
whichever axis the service exposes and maps the accessory’s real degree
range onto 0 to 100, publishing the degrees on the TraitConstraint so
a UI can still label them. Two-axis tilt is not supported: an
accessory offering both gets its horizontal axis, and there is no way
to reach the other one from portable code.
TARGET_COVERING_TILT
public static final Trait TARGET_COVERING_TILTTilt the slats of a blind. Same single-axis model as
COVERING_TILT; read that constant’s note.
HomeKit: TargetHorizontalTilt or TargetVerticalTilt. Matter:
TargetPositionTiltPercent100ths and the GoToTiltPercentage command.
COVERING_MOTION
public static final Trait COVERING_MOTIONWhich way a covering is moving, as a PositionState.
HomeKit: PositionState. Matter: the global bits of Window Covering
0x0102 OperationalStatus.
Reported in terms of opening and closing, not of the position number rising or falling, because the two backends disagree about which direction that number runs.
MOTION_DETECTED
public static final Trait MOTION_DETECTEDWhether motion is being detected.
HomeKit: MotionDetected, a characteristic of its own.
Matter has no motion cluster. A Matter motion sensor is the
Occupancy Sensor device type with a PIR sensor type, so on Matter and
Google Home this trait and OCCUPANCY_DETECTED read the same bit and
always agree.
Both are exposed anyway, rather than hiding one, because an app written against HomeKit’s motion sensor should not have its feature vanish on Android. Just do not treat the two as independent signals.
OCCUPANCY_DETECTED
public static final Trait OCCUPANCY_DETECTEDWhether a space is occupied.
HomeKit: OccupancyDetected. Matter: Occupancy Sensing 0x0406,
bit 0 of Occupancy. See MOTION_DETECTED for why the two coincide
on Matter.
CONTACT_DETECTED
public static final Trait CONTACT_DETECTEDWhether a contact sensor is closed. True means the door or window is shut.
The polarity is worth stating twice because the platforms differ:
HomeKit’s ContactState is an inverted integer where 0 means closed,
and Matter’s Boolean State 0x0045 StateValue is a boolean where
true means closed. This API follows Matter and the iOS port inverts.
LEAK_DETECTED
public static final Trait LEAK_DETECTEDWhether water is being detected.
HomeKit: LeakDetected. Matter: the Water Leak Detector device type
over Boolean State 0x0045, where true means a leak.
Note that Boolean State means the opposite thing here than it does
for CONTACT_DETECTED, because its polarity is fixed by the device
type rather than by the cluster. That is why the ports’ mapping tables
key on device type, cluster and attribute together, never on the
cluster alone.
SMOKE_DETECTED
public static final Trait SMOKE_DETECTEDWhether smoke is being detected, as an AlarmState.
HomeKit: SmokeDetected, two-state, so it can only ever report
AlarmState.NORMAL or AlarmState.CRITICAL.
Matter: Smoke CO Alarm 0x005C, SmokeState, three-state, so
AlarmState.WARNING is reachable there.
Read AlarmState’s note before building anything on this. It is not a
fire-alarm channel.
CO_DETECTED
public static final Trait CO_DETECTEDWhether carbon monoxide is being detected, as an AlarmState.
HomeKit: CarbonMonoxideDetected, two-state. Matter: Smoke CO Alarm
0x005C, COState, three-state. Same asymmetry as
SMOKE_DETECTED, and the same warning.
CO_LEVEL
public static final Trait CO_LEVELMeasured carbon monoxide, in parts per million.
HomeKit: CarbonMonoxideLevel, already ppm.
Matter: Carbon Monoxide Concentration Measurement 0x040C, whose
MeasurementUnit is chosen by the accessory and may be a mass
concentration. Where the accessory reports mass per volume the port
reports no value, because converting needs the gas’s molar mass and
the ambient temperature and pressure, and inventing those would produce
a number that looks like a measurement.
CO2_LEVEL
public static final Trait CO2_LEVELMeasured carbon dioxide, in parts per million.
HomeKit: CarbonDioxideLevel. Matter: Carbon Dioxide Concentration
Measurement 0x040D. Same unit caveat as CO_LEVEL.
AIR_QUALITY
public static final Trait AIR_QUALITYA summarized air-quality rating, as an AirQualityLevel.
HomeKit: AirQuality, six levels. Matter: Air Quality 0x005B, seven.
The enum documents how they are reconciled and which constant HomeKit
can never produce. Do not hard-code thresholds against it and expect
them to describe the same air on both platforms.
PM2_5_DENSITY
public static final Trait PM2_5_DENSITYFine particulate matter, in micrograms per cubic metre.
HomeKit: PM2_5Density. Matter: PM2.5 Concentration 0x042A, where
the accessory’s own MeasurementUnit has to match; see CO_LEVEL.
PM10_DENSITY
public static final Trait PM10_DENSITYCoarse particulate matter, in micrograms per cubic metre.
HomeKit: PM10Density. Matter: PM10 Concentration 0x042D.
VOC_DENSITY
public static final Trait VOC_DENSITYTotal volatile organic compounds.
HomeKit reports these in micrograms per cubic metre; Matter’s Total VOC
Concentration 0x042E is commonly in parts per billion. The unit
therefore varies by accessory, which is exactly why
TraitValue.getDouble(TraitUnit) makes you name the one you expect and
refuses to convert between the two dimensions. Read
TraitValue.getUnit() before you render this one.
BATTERY_LEVEL
public static final Trait BATTERY_LEVELHow much battery an accessory has left, as a percentage.
HomeKit: BatteryLevel, already 0 to 100. Matter: Power Source
0x002F, BatPercentRemaining, which is in half percent and runs
0 to 200. The port halves it; application code sees percent.
BATTERY_CHARGING
public static final Trait BATTERY_CHARGINGWhether the battery is charging, as a ChargingState.
HomeKit: ChargingState. Matter: Power Source 0x002F,
BatChargeState. The enum documents which constants each backend can
never produce – notably that HomeKit cannot tell a full battery on a
charger from one that is running down.
BATTERY_LOW
public static final Trait BATTERY_LOWWhether the battery is low enough to want attention.
HomeKit: StatusLowBattery, two-state. Matter: Power Source 0x002F,
BatChargeLevel, whose warning and critical levels both arrive here as
true – a small loss, and the only one on this trait.
FAN_SPEED
public static final Trait FAN_SPEEDHow fast a fan is running, as a percentage.
HomeKit: RotationSpeed, one characteristic for both reading and
writing.
Matter splits it: Fan Control 0x0202 has PercentSetting for what
was asked and PercentCurrent for what the motor is actually doing.
Writes go to the former and reads come from the latter, so a read
immediately after a write can still show the old speed while the fan
ramps. That is the accessory being honest, not a stale cache.
FAN_MODE
public static final Trait FAN_MODEHow a fan is running, as a FanMode.
Matter: Fan Control 0x0202, FanMode, seven values. HomeKit has no
real equivalent, and the enum documents the translation – including
that low, medium and high are written as speeds on iOS and never read
back. If your UI shows a speed, read FAN_SPEED instead; it behaves
the same everywhere.
VOLUME
public static final Trait VOLUMESpeaker volume, as a percentage.
HomeKit: Volume on a speaker service. Matter: the speaker device
type’s Level Control, 0 to 254, with the same quantization as
BRIGHTNESS.
Expect this to be unsupported on almost every real accessory. HomeKit’s speaker service is in practice only exposed by cameras and doorbells – AirPlay speakers and HomePods are not HomeKit accessories and do not appear in the graph at all – and Matter speakers barely exist in the field. This is not a media-playback API.
MUTE
public static final Trait MUTEWhether a speaker is muted.
HomeKit: Mute, a plain boolean. Matter: the speaker device type uses
On/Off, where off means muted – inverted. The port flips it.
Same availability caveat as VOLUME.
Method details
getId
public String getId()Returns
nullgetValueKind
public TraitValueKind getValueKind()TraitValue getter reads it.Returns
nullgetUnit
public TraitUnit getUnit()TraitValueKind.DOUBLE trait is expressed in.
TraitUnit.NONE for every other kind.Returns
nullacceptsUnit
public boolean acceptsUnit(TraitUnit unit)Whether this trait can only ever be read.
Whether a reading of this trait may arrive in the given unit.
The trait’s own unit always may. So may one of the alternates, which exist for the traits whose javadoc says the unit varies by accessory – VOC arrives in micrograms per cubic metre from HomeKit and in parts per billion from Matter, and refusing the second would refuse a reading this API promises to deliver.
Parameters
unitTraitUnit- the unit the value carries
Returns
true when a value in that unit is a valid reading of this traitenumValue
public TraitValue enumValue(int ordinal)Whether a value belongs to this trait’s domain enum.
Checked by constant name where the value has one, which is every value an application builds, and by ordinal range otherwise – the codec decodes a wire ordinal with no constant in hand.
A value out of the wrong enum is not a curiosity: every enum crosses
the wire as an ordinal, and AlarmState.WARNING and
LockState.UNSECURED are both 1. Asking a door lock for an alarm
state, unguarded, unlocks the door.
Parameters
ordinalint- an ordinal in this trait’s domain
Returns
true when this is not a choice trait, or the value fits its domain
The value an ordinal names in this trait’s domain.
The codec’s way of turning a wire ordinal into the same value an
application would have built for that constant: named, so it is equal
to one built with TraitValue.ofEnum(java.lang.Enum). Reading it back
unnamed made every reading unequal to the value a caller was comparing
it against.
acceptsEnumValue
public boolean acceptsEnumValue(TraitValue value)acceptsEnumWrite
public boolean acceptsEnumWrite(TraitValue value)Whether a value may be WRITTEN to this trait.
Narrower than acceptsEnumValue(TraitValue) for a target enum that
carries states an accessory reports and cannot be asked for: a door
that is OPENING, a thermostat mode HomeKit cannot express. Each enum
says the same thing with its own isWritable(); this is the form the
write path can check when it has an ordinal rather than a constant.
Parameters
valueTraitValue- the value to check, or
null
Returns
true when this is not a choice trait, or the value may be writtenisReadOnly
public boolean isReadOnly()TraitConstraint, not of the trait. This answers the stronger
question of whether writing it could ever mean anything.Returns
true when no accessory can accept a write of this traithasNominalRange
public boolean hasNominalRange()getNominalMinimum() and getNominalMaximum() mean
anything.Returns
true when this trait has a documented rangegetNominalMinimum
public double getNominalMinimum()The bottom of this trait’s documented range, in getUnit().
Documentation, not validation. What a given accessory will actually
accept is on its TraitConstraint, which is where a write is checked;
a dimmer whose real floor is 10 percent says so there.
Returns
hasNominalRange() is falsegetNominalMaximum
public double getNominalMaximum()getUnit(). See
getNominalMinimum().Returns
hasNominalRange() is falseall
public static List<Trait> all()Returns
forId
public static Trait forId(String id)Resolves a trait by its getId(), total: an unknown or null id
answers null rather than throwing.
Total because ids arrive from outside – a persisted favourite, a build hint, a port built against a newer version of this table. An unrecognized one is a value to skip, not a reason to fail a decode that still has good rows in it.
Parameters
idString- a trait identifier, or
null
Returns
null when this release has no such traittoString
public String toString()