Skip to content

Latest commit

 

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CitrineOS Home Assistant Energy Controller

CitrineOS Logo

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

Key Features

1. Automated Local Load Controller

  • 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.

2. Intelligent Operating Modes

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.

3. Multi-Station Power & 3-Phase Balancing

  • 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 as 3-Phase (L1+L2+L3), 1-Phase (L1 / Phase A), 1-Phase (L2 / Phase B), or 1-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), and pause (temporarily suspend).

Entities Provided

Site Hub: Citrine Energy Controller Hub

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 ($6\text{ A} - 16\text{ A}$)
number.citrine_max_ramp_rate Number Maximum power ramp rate ($\text{W/s}$)
button.citrine_recompute_load_control Button Trigger an immediate controller calculation cycle
button.citrine_emergency_safe_mode Button Immediate emergency power curtailment

Discovered Chargers: Citrine Charger <ID>

  • Status Sensors: Online state, Session state, Connector count, Protocol, OCPP Heartbeat age.
  • Control Selects: Control Override (auto, boost, pause), Station Priority (1 - Lowest to 5 - Highest), Phase Wiring (3-Phase, L1, L2, L3).
  • Buttons: Start Charging, Stop Charging, Apply Profile, Clear Profile, Dynamic Session Start/Stop.

Services

  • 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.

Lovelace Dashboard & Automation Blueprints

Turnkey Dashboard Card

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, and Grid Capped modes on the fly.
  • Action Buttons: Instant Recompute and Emergency Safe Mode triggers.
  • Health Indicators: Real-time MQTT stream status and CitrineOS API telemetry freshness.

Automation Blueprints

  • 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) and Solar Only during peak price hours.

Implemented capabilities

  1. 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
  1. 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
  1. Control services
  • citrine_ha.start_charging
  • citrine_ha.stop_charging
  • citrine_ha.set_station_limit
  • citrine_ha.set_group_limit
  • citrine_ha.set_charging_profile
  • citrine_ha.clear_charging_profile
  • citrine_ha.sync_discovery_now
  • citrine_ha.sync_ems_telemetry_now
  • citrine_ha.clear_ems_telemetry_error

Bidirectional note:

  • set_charging_profile supports negative limit values 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 duration to 0 for an indefinite profile (the integration omits duration in the OCPP schedule payload).
  • Charging profile controls and service calls support explicit profile_kind selection. OCPP 2.0.1 exposes Absolute and Relative; OCPP 2.1 additionally exposes Dynamic.
  • OCPP 2.1 stations additionally support Dynamic profile kind and can push both limit and setpoint values.
  • set_charging_profile accepts optional setpoint and profile_periods for advanced multi-period dynamic schedules.
  • OCPP 2.1 dynamic profiles support optional operation_mode and discharge_limit controls.
  • Supported OCPP 2.1 operation modes in HA are: ChargingOnly, ExternalLimits, CentralSetpoint, ExternalSetpoint, LocalFrequency, LocalLoadBalancing, and Idle.
  • Operation-mode controls are capability-gated: non-2.1 stations do not expose the Profile Operation Mode select in HA.
  • Service input normalization matches UI capability rules: if set_charging_profile is called with an unsupported operation_mode, the integration automatically falls back to the station default mode.
  • Changing Profile Limit, Profile Setpoint, or Profile Discharge Limit immediately pushes an updated profile.
  • If a charger applies the profile but drives power in the opposite sign direction, set Profile Sign Mode to invert_negative for that station.
  • Profile Tx Mode controls TxProfile behavior: use safe_fallback for charger compatibility fallbacks, or strict_txprofile to keep TxProfile unchanged.
  1. 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 0 rejection on OCPP 1.6, unit compatibility fallback)
  • OCPP 2.0.1 remoteStartId can 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 Eligibility diagnostic 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, and ems_telemetry_limit for telemetry scoping
  • Options now support ems_telemetry_stale_secs for telemetry stale detection threshold
  • sync_ems_telemetry_now also supports optional per-call overrides for site_id and telemetry_limit
  • EMS intake total attributes now include fetched_at and last_success_at timestamps

EMS profile eligibility rules:

  • eligible means the integration believes the charger can participate in Citrine EMS charging-profile control.
  • ineligible means one of the current conservative guards blocked the charger, for example:
  • protocol is not ocpp2.0.1 or ocpp2.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_reason
  • advertised_capabilities
  • normalized_protocol
  • supports_dynamic_profiles
  • supports_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 Eligibility

If 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_b with your actual Home Assistant entity ids.
  • The integration unique id suffix for this sensor is _ems_profile_eligibility.

Hasura query for stations, connectors, and transactions

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 capabilities in 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/previous transaction id automatically.
  • Start command can auto-select EVSE from connector rows.
  • For OCPP 2.0.1, remoteStartId is derived from station transactions as max(transactionId) + 1 when 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, or identifier.

Load into Home Assistant

  1. Copy this folder into your HA config path:
  • <config>/custom_components/citrine_ha
  1. Restart Home Assistant.

  2. Add integration:

  • Settings -> Devices & Services -> Add Integration -> "CitrineOS HA"
  1. Fill config fields.

  2. Verify devices are created for discovered chargers.

Install via HACS

This repository is now HACS-ready with hacs.json at the repository root.

  1. In Home Assistant, open HACS.
  2. Go to Integrations.
  3. Open the menu and select Custom repositories.
  4. Add your repository URL and choose category Integration.
  5. Search for CitrineOS HA in HACS and install it.
  6. Restart Home Assistant.
  7. Add the integration from Settings -> Devices & Services.

Next hardening tasks (recommended)

  1. Add explicit capability mapping per station (supported units, profile purpose, connector model).
  2. Persist outbox/idempotency keys for retries and restart-safe command execution.
  3. Replace equal-split group allocator with weighted/floor-based policy.
  4. Add reconciliation loop using getChargingProfiles/getCompositeSchedule.
  5. Add test suite (unit + integration mocks).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages