Skip to content

Section 14 Energy

Ben edited this page Nov 1, 2026 · 1 revision

Section 14: Energy Devices

Matter Specification § 14 - Devices that generate, store, use or measure energy.

Note

Every device type on this page requires Homebridge v2.5.0 or later.

Warning

It is not yet known how Apple Home presents these device types. They register and pair, and other Matter controllers may show them before Apple Home does. Test them in the Home app before replacing something that already works there, such as a heating-only Thermostat or an outlet with power monitoring.

All electrical values use the raw Matter units: voltage in millivolts, current in milliamps, power in milliwatts and energy in milliwatt-hours. null means "no reading right now". The electricalPowerMeasurement and electricalEnergyMeasurement clusters work exactly as they do on an outlet - see Smart Plugs (§5) for the full description.


Composed Energy Devices

Solar Power, Battery Storage and the EV Charger are composed devices: the parent carries the device type, and its parts carry what the spec requires of it. The parts use these device types:

Part device type Becomes
api.matter?.deviceTypes.PowerSource A power source. Declare bat* attributes for a battery (it gets the Battery and Rechargeable features), or only wired attributes for mains, an inverter or the grid (it gets the Wired feature).
api.matter?.deviceTypes.ElectricalSensor A meter. Its clusters come from the declared electricalPowerMeasurement / electricalEnergyMeasurement state. powerMode: 1 makes it measure DC; the default is AC.
api.matter?.deviceTypes.DeviceEnergyManagement Lets a controller ask the device to change its power use. Its features come from the declared state (see below).

Important

A composed energy device must have exactly the parts the spec lists, with the attributes it lists for each. matter.js checks this when the accessory registers and logs any gap as violates device type requirements, naming the missing attribute or feature. Run Homebridge with -D and check the log after adding one.

When testing a change, start from fresh storage or remove the cached accessory first. A restored accessory keeps its cached state, so new clusters values never reach it.

DeviceEnergyManagement Features

Declared in deviceEnergyManagement Feature turned on
powerAdjustmentCapability PowerAdjustment
forecast PowerForecastReporting

Other features (Pausable, StartTimeAdjustment and so on) can be composed by hand with api.matter?.deviceRequirements.DeviceEnergyManagement - see Customising Features. matter.js implements none of this cluster's commands, so Homebridge records each request in the state; supply deviceEnergyManagement handlers to act on them.


EV Charger

Property Value
Device Type api.matter?.deviceTypes.EnergyEvse
Description Electric vehicle supply equipment - a car charger.
Matter Specification § 14.1

The charger's own endpoint carries two clusters:

  • energyEvse - whether a car is plugged in, whether charging is allowed, and the current limits
  • energyEvseMode - its modes, which need supportedModes and currentMode. One mode must be tagged Manual (0x4000); others can be tagged Time of Use (0x4001), Solar Charging (0x4002) or V2X (0x4003)

and three parts: a wired PowerSource, an ElectricalSensor for the power going to the car, and a DeviceEnergyManagement part declaring a forecast.

Optional energyEvse attributes turn on extra features: stateOfCharge or batteryCapacity add SoCReporting, vehicleId adds PlugAndCharge, and any discharging attribute (such as maximumDischargeCurrent) adds V2X.

energyEvse Attributes

Attribute Type Description
state number | null 0=not plugged in, 1=plugged in, 2=plugged in and wants charge, 3=charging, 4=discharging, 5=session ending, 6=fault
supplyState number 0=disabled, 1=charging enabled, 2=discharging enabled, 3=disabled by an error, 4=disabled for diagnostics, 5=enabled
faultState number 0=no error (see the spec for the fault codes)
chargingEnabledUntil number | null When charging stops being allowed; null = until told otherwise
circuitCapacity number The circuit's limit, in mA
minimumChargeCurrent number The lowest current to charge at, in mA (6 A is usual)
maximumChargeCurrent number The highest current allowed right now, in mA

Declaring State

deviceType: api.matter?.deviceTypes.EnergyEvse,
clusters: {
  energyEvse: {
    state: 1, // plugged in
    supplyState: 0, // disabled until a controller enables charging
    faultState: 0,
    chargingEnabledUntil: null,
    circuitCapacity: 32_000, // 32 A
    minimumChargeCurrent: 6_000, // 6 A
    maximumChargeCurrent: 0,
  },
  energyEvseMode: {
    supportedModes: [
      { label: 'Manual', mode: 0, modeTags: [{ value: 0x4000 }] },
      { label: 'Cheapest Times', mode: 1, modeTags: [{ value: 0x4001 }] },
      { label: 'Solar Only', mode: 2, modeTags: [{ value: 0x4002 }] },
    ],
    currentMode: 0,
  },
},
parts: [
  {
    id: 'supply',
    displayName: 'Supply',
    deviceType: api.matter?.deviceTypes.PowerSource,
    clusters: {
      powerSource: { status: 1, order: 0, description: 'Mains', wiredCurrentType: 0 },
    },
  },
  {
    id: 'meter',
    displayName: 'Charging Power',
    deviceType: api.matter?.deviceTypes.ElectricalSensor,
    clusters: {
      electricalPowerMeasurement: { voltage: 230_000, activeCurrent: 0, activePower: 0 },
      electricalEnergyMeasurement: { cumulativeEnergyImported: { energy: 0 } },
    },
  },
  {
    id: 'energy-management',
    displayName: 'Energy Management',
    deviceType: api.matter?.deviceTypes.DeviceEnergyManagement,
    clusters: {
      deviceEnergyManagement: {
        esaType: 0, // EVSE
        esaCanGenerate: false,
        esaState: 1, // online
        absMinPower: 0,
        absMaxPower: 7_400_000, // 7.4 kW
        forecast: null, // turns on PowerForecastReporting
      },
    },
  },
],
Handlers

matter.js implements none of the charger's commands. Without a handler, Homebridge records each command in the state; with one, your handler runs first. There is no getTargets handler - Homebridge answers it with the targets the last setTargets gave.

handlers: {
  energyEvse: {
    enableCharging: async ({ chargingEnabledUntil, minimumChargeCurrent, maximumChargeCurrent }) => {
      await myChargerAPI.enableCharging(Number(maximumChargeCurrent) / 1000)
    },
    disable: async () => {
      await myChargerAPI.disable()
    },
    // also: enableDischarging, startDiagnostics, setTargets, clearTargets
  },
  energyEvseMode: {
    changeToMode: async ({ newMode }) => {
      await myChargerAPI.setMode(newMode)
    },
  },
}

The example accessory in this plugin is EvChargerAccessory (option enableEvCharger).


Water Heater

Property Value
Device Type api.matter?.deviceTypes.WaterHeater
Description A hot water tank or boiler, with operating modes and a boost.
Matter Specification § 14.2

A water heater carries three clusters:

  • thermostat - heating only. Declare the water temperature as externalMeasuredIndoorTemperature and the target as occupiedHeatingSetpoint
  • waterHeaterMode - needs supportedModes and currentMode. One mode must be tagged Off (0x4000) and one Manual (0x4001); Timed (0x4002) is optional. A mode tagged Off, Manual or Timed carries no other tag
  • waterHeaterManagement - needs heaterTypes, heatDemand and boostState

matter.js will not build the endpoint without every one of those.

Important

Declare absMinHeatSetpointLimit and absMaxHeatSetpointLimit too. They default to a room thermostat's 7°C - 30°C, and matter.js pulls any higher limit or target down to fit - a 55°C target becomes 30°C.

Declaring State

deviceType: api.matter?.deviceTypes.WaterHeater,
clusters: {
  thermostat: {
    externalMeasuredIndoorTemperature: 4800, // water at 48.00°C
    occupiedHeatingSetpoint: 5500,           // heat to 55.00°C
    absMinHeatSetpointLimit: 3000,           // 30.00°C
    absMaxHeatSetpointLimit: 6500,           // 65.00°C
    minHeatSetpointLimit: 3000,
    maxHeatSetpointLimit: 6500,
    controlSequenceOfOperation: 2,           // heating only
    systemMode: 4,                           // heat
  },
  waterHeaterMode: {
    supportedModes: [
      { label: 'Off', mode: 0, modeTags: [{ value: 0x4000 }] },
      { label: 'Manual', mode: 1, modeTags: [{ value: 0x4001 }] },
      { label: 'Timed', mode: 2, modeTags: [{ value: 0x4002 }] },
    ],
    currentMode: 1,
  },
  waterHeaterManagement: {
    heaterTypes: { immersionElement1: true, heatPump: true }, // the heat sources it has
    heatDemand: {},                                           // which are heating now
    boostState: 0,                                            // 0=inactive, 1=active
    // tankVolume, estimatedHeatRequired and tankPercentage need features that
    // Homebridge does not turn on from state - compose the cluster first, see
    // Customising Features
  },
},
Handlers

matter.js leaves boost unimplemented, so Homebridge records it: once your boost handler returns, boostState becomes active. Ending the boost is up to the plugin - set boostState back to 0 when the duration runs out.

handlers: {
  thermostat: {
    occupiedHeatingSetpointChange: async ({ occupiedHeatingSetpoint }) => {
      await myWaterHeaterAPI.setTargetTemperature(occupiedHeatingSetpoint / 100)
    },
  },
  waterHeaterMode: {
    changeToMode: async ({ newMode }) => {
      await myWaterHeaterAPI.setMode(newMode)
    },
  },
  waterHeaterManagement: {
    boost: async ({ boostInfo }) => {
      // boostInfo.duration is in seconds; also oneShot, emergencyBoost,
      // temporarySetpoint, targetPercentage and targetReheat
      await myWaterHeaterAPI.boost(boostInfo.duration)
      setTimeout(() => {
        void api.matter?.updateAccessoryState(uuid, 'waterHeaterManagement', { boostState: 0 })
      }, boostInfo.duration * 1000)
    },
    cancelBoost: async () => {
      await myWaterHeaterAPI.cancelBoost()
    },
  },
}

The example accessory in this plugin is WaterHeaterAccessory (option enableWaterHeater).


Solar Power

Property Value
Device Type api.matter?.deviceTypes.SolarPower
Description A solar inverter, reporting what it generates.
Matter Specification § 14.3

A composed device with two parts: a wired PowerSource (the inverter's output) and an ElectricalSensor reporting voltage, current, power and cumulativeEnergyExported. Generation is reported as negative active power: power flowing out of the device.

Declaring State

deviceType: api.matter?.deviceTypes.SolarPower,
parts: [
  {
    id: 'inverter',
    displayName: 'Inverter',
    deviceType: api.matter?.deviceTypes.PowerSource,
    clusters: {
      powerSource: { status: 1, order: 0, description: 'Inverter', wiredCurrentType: 0 },
    },
  },
  {
    id: 'meter',
    displayName: 'Generation',
    deviceType: api.matter?.deviceTypes.ElectricalSensor,
    clusters: {
      electricalPowerMeasurement: {
        voltage: 230_000,
        activeCurrent: -10_870,
        activePower: -2_500_000, // generating 2.5 kW
      },
      electricalEnergyMeasurement: {
        cumulativeEnergyExported: { energy: 0 },
      },
    },
  },
],

Update a part's state by passing its id as the last argument:

await api.matter?.updateAccessoryState(uuid, 'electricalPowerMeasurement', { activePower: -3_100_000 }, 'meter')

The example accessory in this plugin is SolarPowerAccessory (option enableSolarPower).


Battery Storage

Property Value
Device Type api.matter?.deviceTypes.BatteryStorage
Description A home battery that stores energy and gives it back.
Matter Specification § 14.4

A composed device with five parts:

  • a battery PowerSource. On top of the usual battery attributes, the spec requires batVoltage, batCapacity (mAh), batTimeRemaining, batTimeToFullCharge, batChargingCurrent, activeBatFaults and activeBatChargeFaults - declare null or [] for any you cannot report yet
  • a wired PowerSource - the grid side
  • an ElectricalSensor for the AC side and one for the DC side. Both must report power and energy, and the DC one declares powerMode: 1
  • a DeviceEnergyManagement part declaring powerAdjustmentCapability, so a controller can ask the battery to charge or discharge at a set power

Positive power is charging (into the battery), negative is discharging.

Declaring State

deviceType: api.matter?.deviceTypes.BatteryStorage,
parts: [
  {
    id: 'battery',
    displayName: 'Battery',
    deviceType: api.matter?.deviceTypes.PowerSource,
    clusters: {
      powerSource: {
        status: 1, order: 0, description: 'Battery',
        batPercentRemaining: 160,  // half-percent steps: 160 = 80%
        batChargeLevel: 0,         // ok
        batReplacementNeeded: false,
        batReplaceability: 3,      // factory replaceable
        batChargeState: 3,         // not charging
        batFunctionalWhileCharging: true,
        batVoltage: 400_000,       // mV
        batCapacity: 33_750,       // mAh
        batTimeRemaining: null,
        batTimeToFullCharge: null,
        batChargingCurrent: null,
        activeBatFaults: [],
        activeBatChargeFaults: [],
      },
    },
  },
  {
    id: 'grid',
    displayName: 'Grid Connection',
    deviceType: api.matter?.deviceTypes.PowerSource,
    clusters: {
      powerSource: { status: 1, order: 1, description: 'Grid', wiredCurrentType: 0 },
    },
  },
  {
    id: 'ac-meter',
    displayName: 'AC Side',
    deviceType: api.matter?.deviceTypes.ElectricalSensor,
    clusters: {
      electricalPowerMeasurement: { voltage: 230_000, activeCurrent: 0, activePower: 0 },
      electricalEnergyMeasurement: {
        cumulativeEnergyImported: { energy: 0 },
        cumulativeEnergyExported: { energy: 0 },
      },
    },
  },
  {
    id: 'dc-meter',
    displayName: 'DC Side',
    deviceType: api.matter?.deviceTypes.ElectricalSensor,
    clusters: {
      electricalPowerMeasurement: { powerMode: 1, voltage: 400_000, activeCurrent: 0, activePower: 0 },
      electricalEnergyMeasurement: {
        cumulativeEnergyImported: { energy: 0 },
        cumulativeEnergyExported: { energy: 0 },
      },
    },
  },
  {
    id: 'energy-management',
    displayName: 'Energy Management',
    deviceType: api.matter?.deviceTypes.DeviceEnergyManagement,
    clusters: {
      deviceEnergyManagement: {
        esaType: 5,           // battery storage
        esaCanGenerate: true,
        esaState: 1,          // online
        absMinPower: -5_000_000,
        absMaxPower: 5_000_000,
        powerAdjustmentCapability: {
          powerAdjustCapability: [
            { minPower: -5_000_000, maxPower: 5_000_000, minDuration: 60, maxDuration: 3600 },
          ],
          cause: 0, // normal operation
        },
        optOutState: 0,
      },
    },
    handlers: {
      deviceEnergyManagement: {
        powerAdjustRequest: async ({ power, duration }) => {
          await myBatteryAPI.setPower(Number(power) / 1000, duration)
        },
        cancelPowerAdjustRequest: async () => {
          await myBatteryAPI.resumeNormalOperation()
        },
      },
    },
  },
],

The example accessory in this plugin is BatteryStorageAccessory (option enableBatteryStorage).


Electrical Meter

Property Value
Device Type api.matter?.deviceTypes.ElectricalMeter
Description A meter on a circuit or the whole home.
Matter Specification § 14.8

Not composed: declare the measurement clusters on the accessory itself. Like an outlet's Electrical Sensor, the features come from the attributes you declare - declaring both cumulativeEnergyImported and cumulativeEnergyExported gives a meter that counts both ways.

deviceType: api.matter?.deviceTypes.ElectricalMeter,
clusters: {
  electricalPowerMeasurement: {
    voltage: 230_000,
    activeCurrent: 4_350,
    activePower: 1_000_000, // 1 kW drawn from the grid
  },
  electricalEnergyMeasurement: {
    cumulativeEnergyImported: { energy: 1_250_000_000 }, // 1250 kWh
    cumulativeEnergyExported: { energy: 310_000_000 },   // 310 kWh
  },
},

The example accessory in this plugin is ElectricalMeterAccessory (option enableElectricalMeter).


Utility Meter and Tariff

api.matter?.deviceTypes.ElectricalUtilityMeter (§ 14.9) and ElectricalEnergyTariff (§ 14.7) are also available. Both are Matter 1.5 types, so controllers may not know them yet, and this plugin has no examples of them.

  • A utility meter is the energy company's own meter: declare meterIdentification state (meterType, pointOfDelivery, meterSerialNumber, protocolVersion).
  • A tariff carries no cluster of its own: compose CommodityPrice or CommodityTariff from api.matter?.deviceRequirements.ElectricalEnergyTariff, and give it the 1 to 6 descriptor tags it requires as clusters.descriptor.tagList.

Clone this wiki locally