A powerful, user-friendly Home Assistant integration and local load controller for CitrineOS. It bridges Home Assistant's local sensing (grid meter, solar inverter, home battery) with CitrineOS's OCPP smart charging layer to provide automated solar surplus tracking, main fuse protection, priority EV power allocation, and CSIP-Aus dynamic operating envelopes.
- Domain:
citrine_ha - Path:
custom_components/citrine_ha - Supports UI Config Flow & full Options Flow
- Multi-mode real-time closed loop energy governor (1-2s cycle)
- Automated solar surplus & battery discharge allocation
- Dynamic multi-station priority weighting and minimum pilot current (
$6\text{ A}$ ) protection - Dual device architecture: Citrine Energy Controller Hub & Per-Station Charger Controls
- Real-time Closed Loop: Continuously evaluates grid import/export, solar generation, and home battery power every 1–3 seconds.
- Main Fuse Protection: Enforces hard site import limits with instantaneous asymmetric curtailment if household loads spike.
-
Anti-Chatter & Dwell Protection: Enforces a
$6\text{ A}$ minimum pilot floor ($1.38\text{ kW}$ 1-ph /$4.14\text{ kW}$ 3-ph) and minimum 30-second dwell time to protect EV contactors against intermittent cloud cover or appliance cycling. -
Deadband Slew Limiting: Power changes smaller than the deadband (default
$250\text{ W}$ ) are filtered out to prevent unnecessary network overhead and relay wear.
Selectable on the fly via select.citrine_controller_mode:
- Solar Only: Charges strictly from excess solar generation above a configurable start buffer.
- Solar + Battery: Uses excess solar and allows drawing from the home battery down to a minimum configurable SOC.
- Grid Capped (Fast): Maximizes EV charging speed while ensuring total site load stays safely below the main grid fuse rating.
- Dynamic Envelope (DOE): Follows dynamic utility operating envelopes (CSIP-Aus / IEEE 2030.5). Ingests real-time import/export caps from your CSIP gateway/client sensor (e.g. Edge zero, Wattwatchers, SwitchDin) and dynamically limits EV share while co-optimizing with other household loads.
- Off / Emergency Safe: Suspends all charging or drops power to safe minimums immediately.
- Dynamically balances available site power across multiple connected EVSEs.
- Supports Per-Station Priority (1 to 5): higher-priority vehicles receive power first.
- Supports Per-Station Phase Wiring (
select.<id>_phase_wiring): configure chargers as3-Phase (L1+L2+L3),1-Phase (L1 / Phase A),1-Phase (L2 / Phase B), or1-Phase (L3 / Phase C). - Phase-Unbalance Protection: Ingests Phase A, B, and C grid currents and enforces maximum phase-to-phase unbalance limits (e.g., AS/NZS 4777 20A limit) so high 1-phase EV loads do not trip main neutral or phase fuses.
- Supports Per-Station Mode Overrides:
auto(follows site controller),boost(max power override), andpause(temporarily suspend).
| Entity | Type | Description |
|---|---|---|
select.citrine_controller_mode |
Select | Active site mode (Solar Only, Solar + Battery, Grid Capped, Off, etc.) |
sensor.citrine_controller_state |
Sensor | State summary (Tracking Solar, Curtailing, Normal, Safe Fallback) |
sensor.citrine_allocated_ev_power |
Power Sensor | Real-time Watts allocated across all EVSEs |
sensor.citrine_site_headroom_power |
Power Sensor | Remaining Watts before reaching main fuse limit |
sensor.citrine_solar_surplus_power |
Power Sensor | Available excess solar generation in Watts |
sensor.citrine_phase_unbalance_a |
Current Sensor | Real-time phase-to-phase current unbalance (Amps) |
sensor.citrine_phase_a_current |
Current Sensor | Grid Phase A / L1 current (Amps) |
sensor.citrine_phase_b_current |
Current Sensor | Grid Phase B / L2 current (Amps) |
sensor.citrine_phase_c_current |
Current Sensor | Grid Phase C / L3 current (Amps) |
sensor.citrine_active_ev_count |
Sensor | Number of actively charging / connected EVs |
number.citrine_main_fuse_limit |
Number | Site main fuse import limit (Watts) |
number.citrine_solar_start_buffer |
Number | Surplus buffer required before starting charging (Watts) |
number.citrine_min_charge_current |
Number | Pilot current floor ( |
number.citrine_max_ramp_rate |
Number | Maximum power ramp rate ( |
button.citrine_recompute_load_control |
Button | Trigger an immediate controller calculation cycle |
button.citrine_emergency_safe_mode |
Button | Immediate emergency power curtailment |
- Status Sensors: Online state, Session state, Connector count, Protocol, OCPP Heartbeat age.
- Control Selects: Control Override (
auto,boost,pause), Station Priority (1 - Lowestto5 - Highest), Phase Wiring (3-Phase,L1,L2,L3). - Buttons: Start Charging, Stop Charging, Apply Profile, Clear Profile, Dynamic Session Start/Stop.
citrine_ha.set_controller_mode— Change active site control mode.citrine_ha.recompute_load_control— Force an immediate control cycle.citrine_ha.emergency_safe_mode— Immediately curtail EV loads.citrine_ha.start_charging/citrine_ha.stop_charging— Remote transaction controls.citrine_ha.set_station_limit/citrine_ha.set_group_limit— Set static limits.citrine_ha.set_charging_profile/citrine_ha.clear_charging_profile— Advanced OCPP profile dispatch.
A pre-configured Lovelace dashboard view is available in assets/dashboard.yaml featuring:
- Real-Time Gauges: Allocated EV Power, Available Solar Surplus, and Grid Fuse Headroom.
- Operating Mode Switcher & Sliders: Switch between
Solar Only,Solar + Battery, andGrid Cappedmodes on the fly. - Action Buttons: Instant Recompute and Emergency Safe Mode triggers.
- Health Indicators: Real-time MQTT stream status and CitrineOS API telemetry freshness.
- Dynamic Tariff Mode Switcher: blueprints/citrine_tariff_automator.yaml automatically toggles between
Grid Capped (Fast)during cheap/negative spot price windows (e.g. Amber Electric, Nord Pool, Tibber) andSolar Onlyduring peak price hours.
- UI configuration (Config Entry)
- CitrineOS base URL
- Tenant ID
- Bearer token
- SSL verify toggle
- Request timeout
- Optional Hasura URL/token/query
- Discovery scan interval
- Default idTag and EVSE id for quick start commands
- Discovery and entities
- Polls Hasura with GraphQL and maps discovered stations into HA devices
- Sensor per station for online state + metadata
- Additional diagnostics sensors for protocol, connector count, session state, and OCPP heartbeat age
- Additional per-station EMS diagnostic sensor for charging-profile eligibility (
eligible/ineligible) - Integration-level EMS diagnostics sensors for intake totals, accepted count, and rejected count
- Integration-level EMS telemetry freshness diagnostic sensor (seconds since last successful telemetry fetch)
- Integration-level EMS telemetry health status sensor (
ok,stale,error,unknown) - Number per station for max limit (W)
- Start/Stop button entities per station
- Dedicated charging profile UI entities (numbers, selects, and action buttons) so users can apply/clear profiles from dashboards without manual service calls
- Control services
citrine_ha.start_chargingcitrine_ha.stop_chargingcitrine_ha.set_station_limitcitrine_ha.set_group_limitcitrine_ha.set_charging_profilecitrine_ha.clear_charging_profilecitrine_ha.sync_discovery_nowcitrine_ha.sync_ems_telemetry_nowcitrine_ha.clear_ems_telemetry_error
Bidirectional note:
set_charging_profilesupports negativelimitvalues for stations that advertise bidirectional profile support in the integration capability cache (typically OCPP 2.x).- Stations mapped as non-bidirectional (for example OCPP 1.6 by default) will reject negative profile limits with a clear error.
- Set
durationto0for an indefinite profile (the integration omits duration in the OCPP schedule payload). - Charging profile controls and service calls support explicit
profile_kindselection. OCPP 2.0.1 exposesAbsoluteandRelative; OCPP 2.1 additionally exposesDynamic. - OCPP 2.1 stations additionally support
Dynamicprofile kind and can push bothlimitandsetpointvalues. set_charging_profileaccepts optionalsetpointandprofile_periodsfor advanced multi-period dynamic schedules.- OCPP 2.1 dynamic profiles support optional
operation_modeanddischarge_limitcontrols. - Supported OCPP 2.1 operation modes in HA are:
ChargingOnly,ExternalLimits,CentralSetpoint,ExternalSetpoint,LocalFrequency,LocalLoadBalancing, andIdle. - Operation-mode controls are capability-gated: non-2.1 stations do not expose the
Profile Operation Modeselect in HA. - Service input normalization matches UI capability rules: if
set_charging_profileis called with an unsupportedoperation_mode, the integration automatically falls back to the station default mode. - Changing
Profile Limit,Profile Setpoint, orProfile Discharge Limitimmediately pushes an updated profile. - If a charger applies the profile but drives power in the opposite sign direction, set
Profile Sign Modetoinvert_negativefor that station. Profile Tx Modecontrols TxProfile behavior: usesafe_fallbackfor charger compatibility fallbacks, orstrict_txprofileto keepTxProfileunchanged.
- Protocol-aware API calls
- OCPP 2.0.1 and OCPP 1.6 start/stop mappings
- OCPP 2.0.1 and OCPP 1.6 smart charging profile mappings for limits
- OCPP protocol normalization for mixed station metadata formats (for example
ocpp16,1.6,OCPP 2.0) - Endpoint fallback workarounds for deployments exposing
/ocpp/2.0/*instead of/ocpp/2.0.1/* - Retry workarounds for common profile issues (connector
0rejection on OCPP 1.6, unit compatibility fallback) - OCPP 2.0.1
remoteStartIdcan be sourced from transactions and incremented per station - Per-station protocol and capability cache guides entity options and command payload selection
- EMS profile-control entities are now guarded by the same CSMS-facing support rules used by the EMS policy slice
- If a charger is ineligible for EMS profile control, the
EMS Profile Eligibilitydiagnostic sensor exposes the exact support reason and the profile-control entities are unavailable in HA - EMS telemetry consumption via Citrine Data API endpoint
GET /data/<ems-endpoint-prefix>/emsIntakeTelemetry - Options now support
ems_endpoint_prefix,ems_telemetry_site_id, andems_telemetry_limitfor telemetry scoping - Options now support
ems_telemetry_stale_secsfor telemetry stale detection threshold sync_ems_telemetry_nowalso supports optional per-call overrides forsite_idandtelemetry_limit- EMS intake total attributes now include
fetched_atandlast_success_attimestamps
EMS profile eligibility rules:
eligiblemeans the integration believes the charger can participate in Citrine EMS charging-profile control.ineligiblemeans one of the current conservative guards blocked the charger, for example:- protocol is not
ocpp2.0.1orocpp2.1 - station advertised capabilities exist but do not include
ChargingProfileCapable - the integration could not derive a compatible EMS profile-control path
The EMS Profile Eligibility sensor attributes include:
ems_profile_support_reasonadvertised_capabilitiesnormalized_protocolsupports_dynamic_profilessupports_set_charging_profile
Example Lovelace card:
type: entities
title: Citrine EMS Profile Eligibility
entities:
- entity: sensor.station_a_ems_profile_eligibility
name: Station A EMS Profile Eligibility
- entity: sensor.station_b_ems_profile_eligibility
name: Station B EMS Profile EligibilityIf you want to expose the support reason inline, use an attribute row card or template entity. For example with Mushroom template cards:
type: vertical-stack
cards:
- type: entities
title: Citrine EMS Profile Eligibility
entities:
- entity: sensor.station_a_ems_profile_eligibility
- entity: sensor.station_b_ems_profile_eligibility
- type: markdown
content: >
**Station A reason:**
{{ state_attr('sensor.station_a_ems_profile_eligibility', 'ems_profile_support_reason') or 'Eligible' }}
**Station B reason:**
{{ state_attr('sensor.station_b_ems_profile_eligibility', 'ems_profile_support_reason') or 'Eligible' }}Entity naming note:
- Replace
station_a/station_bwith your actual Home Assistant entity ids. - The integration unique id suffix for this sensor is
_ems_profile_eligibility.
Use this as the discovery query value in the config flow/options:
query ChargingStations($tenantId: Int!) {
ChargingStations(where: {tenantId: {_eq: $tenantId}}) {
id
protocol
isOnline
chargePointVendor
chargePointModel
chargePointSerialNumber
firmwareVersion
tenantId
locationId
latestOcppMessageTimestamp
updatedAt
capabilities
}
Connectors(where: {tenantId: {_eq: $tenantId}}) {
id
stationId
chargingStationId
connectorId
evseId
status
isOnline
updatedAt
}
Transactions(where: {tenantId: {_eq: $tenantId}}, order_by: {updatedAt: desc}, limit: 500) {
id
stationId
chargingStationId
transactionId
isActive
active
startedAt
stoppedAt
updatedAt
}
}Notes:
- The integration now merges station + connector + transaction rows.
- Include
capabilitiesin the station query if your Hasura schema exposes it; this allows HA to distinguish explicit lack of charging-profile support from missing metadata. - Stop button and stop service can use discovered
active/current/previoustransaction id automatically. - Start command can auto-select EVSE from connector rows.
- For OCPP 2.0.1,
remoteStartIdis derived from station transactions asmax(transactionId) + 1when numeric, then incremented after each start. - If your table/column names differ, adjust the query in options; the merge logic accepts station references from
stationId,chargingStationId, oridentifier.
- Copy this folder into your HA config path:
<config>/custom_components/citrine_ha
-
Restart Home Assistant.
-
Add integration:
- Settings -> Devices & Services -> Add Integration -> "CitrineOS HA"
-
Fill config fields.
-
Verify devices are created for discovered chargers.
This repository is now HACS-ready with hacs.json at the repository root.
- In Home Assistant, open HACS.
- Go to Integrations.
- Open the menu and select Custom repositories.
- Add your repository URL and choose category Integration.
- Search for CitrineOS HA in HACS and install it.
- Restart Home Assistant.
- Add the integration from Settings -> Devices & Services.
- Add explicit capability mapping per station (supported units, profile purpose, connector model).
- Persist outbox/idempotency keys for retries and restart-safe command execution.
- Replace equal-split group allocator with weighted/floor-based policy.
- Add reconciliation loop using
getChargingProfiles/getCompositeSchedule. - Add test suite (unit + integration mocks).