Skip to content

Latest commit

 

History

History
972 lines (836 loc) · 37.1 KB

File metadata and controls

972 lines (836 loc) · 37.1 KB

WebSocket API

Velair exposes WebSocket commands for the sidebar panel and optional Lovelace card.

Frontend code should use Home Assistant's WebSocket connection:

await hass.connection.sendMessagePromise({
  type: "velair/get_schedule",
});

Schedule Response

Most write commands return the full schedule response:

The response includes a runtime-only zone_runtime mapping. It is derived by the backend and never persisted. Each managed climate reports an authoritative state (stopped, paused, boost, preconditioning, scheduled, idle, or externally_managed) plus the minimal available room, target, applied setpoint, HVAC mode, and relevant timing fields used by Overview.

{
  "configured_entities": ["climate.living_room"],
  "temperature_unit": "°C",
  "home_assistant_temperature_unit": "°C",
  "temperature_migration": {
    "required": false,
    "reason": null,
    "source_unit": "°C",
    "target_unit": "°C",
    "temperature_revision": 3
  },
  "operation_recovery": null,
  "global": {
    "mode": "auto",
    "paused_started_at": null,
    "paused_until": null,
    "active_profile_ids": ["away"],
    "active_mode_id": "away-mode"
  },
  "active_profile_ids": ["away"],
  "profiles": [
    {
      "key": "away",
      "name": "Away",
      "icon": "mdi:home-export-outline",
      "color": "#546e7a",
      "description": "Lower demand while nobody is home",
      "zones": {
        "climate.living_room": {
          "behavior": "schedule",
          "schedule": {
            "monday": []
          }
        }
      }
    }
  ],
  "modes": [
    {
      "key": "away-mode",
      "name": "Away",
      "profile_ids": ["away"]
    }
  ],
  "active_mode_id": "away-mode",
  "settings": {
    "first_weekday": "monday",
    "zone_order": ["climate.living_room"],
    "min_temperature": 5,
    "max_temperature": 35,
    "apply_active_schedule_on_startup": false
  },
  "zones": {
    "climate.living_room": {
      "enabled": true,
      "schedule": {
        "monday": [
          {
            "start": "06:00",
            "action": "set_temperature",
            "target_temp_low": 20,
            "target_temp_high": 24,
            "hvac_mode": "heat_cool",
            "fan_mode": "quiet",
            "preset_mode": "eco",
            "swing_mode": "vertical",
            "swing_horizontal_mode": "left",
            "humidity": 45
          }
        ]
      },
      "override": null,
      "preconditioning": {
        "enabled": false,
        "max_lead_minutes": 1440,
        "minimum_delta_temperature": 0.3,
        "learning_history_size": 120,
        "similar_sample_count": 25,
        "comfort_percentile": 80,
        "adaptive_percentile_enabled": true,
        "partial_expiry_days": 30,
        "recency_decay_days": 30,
        "min_start_minutes": 10,
        "fallback_minutes_per_degree": 25,
        "use_outdoor_temperature": true,
        "outdoor_temperature_entity_id": "sensor.outdoor_temperature",
        "room_temperature_entity_id": "sensor.living_room_temperature",
        "room_sensor_assist_enabled": false,
        "room_sensor_assist_deadband": 0.3,
        "room_sensor_assist_max_delta": 2.0
      },
      "comfort": {
        "enabled": false,
        "temperature_entity_id": null,
        "humidity_enabled": true,
        "humidity_entity_id": null,
        "co2_entity_id": null,
        "temperature_min": 20.0,
        "temperature_max": 24.0,
        "humidity_min": 40.0,
        "humidity_max": 60.0,
        "co2_attention": 1000,
        "co2_poor": 1500,
        "stale_after_minutes": 120
      }
    }
  },
  "operational_status": "scheduled",
  "next_event": null,
  "next_events": [],
  "active_overrides": {},
  "room_sensor_assist": {
    "climate.living_room": {
      "status": "assisting",
      "enabled": true,
      "configured": true,
      "room_temperature_entity_id": "sensor.living_room_temperature",
      "target_temperature": null,
      "target_temp_low": 20,
      "target_temp_high": 24,
      "applied_temperature": null,
      "applied_target_temp_low": 21,
      "applied_target_temp_high": 25,
      "climate_target_temperature": null,
      "climate_target_temp_low": 21,
      "climate_target_temp_high": 25,
      "room_temperature": 19.8,
      "climate_temperature": 20.5,
      "assist_delta": 0.2,
      "applied_offset": null,
      "range_shift": 1.0,
      "limited_by": null,
      "limit_temperature": null,
      "requested_temperature": null,
      "calculated_temperature": null,
      "scheduled_target_guard": null,
      "requested_target_temp_low": null,
      "requested_target_temp_high": null,
      "direction": "heat",
      "hvac_mode": "heat_cool",
      "weekday": "monday",
      "start": "06:00"
    }
  },
  "comfort": {
    "climate.living_room": {
      "enabled": false,
      "condition": "monitoring_off",
      "air_quality": "not_monitored",
      "data_quality": "unavailable",
      "data_issues": []
    }
  },
  "zone_runtime": {
    "climate.living_room": {
      "state": "scheduled",
      "room_temperature": 19.8,
      "target_temperature": null,
      "target_temp_low": 20,
      "target_temp_high": 24,
      "applied_temperature": null,
      "hvac_mode": "heat_cool",
      "active_from": "2026-05-19T06:00:00+00:00",
      "target_when": null
    }
  },
  "preconditioning_learning": {
    "climate.living_room": {
      "status": "learning",
      "required_samples": 5,
      "total_samples": 0,
      "heat": {
        "status": "learning",
        "sample_count": 0,
        "total_samples": 0,
        "required_samples": 5,
        "effective_lead_minutes": null,
        "effective_lead_source": "initial_model",
        "partial_sample_count": 0,
        "complete_sample_count": 0,
        "invalid_sample_count": 0,
        "lead_limited_by_max": false,
        "last_quality": null,
        "model_source": "initial_model",
        "comfort_percentile": 80,
        "similar_sample_count": 25
      },
      "cool": {
        "status": "learning",
        "sample_count": 0,
        "total_samples": 0,
        "required_samples": 5,
        "effective_lead_minutes": null,
        "effective_lead_source": "initial_model",
        "partial_sample_count": 0,
        "complete_sample_count": 0,
        "invalid_sample_count": 0,
        "lead_limited_by_max": false,
        "last_quality": null,
        "model_source": "initial_model",
        "comfort_percentile": 80,
        "similar_sample_count": 25
      }
    }
  },
  "templates": [],
  "versions": {
    "export_format": "velair_portable_data",
    "portable_model": 8,
    "storage": 1,
    "model": 7,
    "integration": "1.7.0"
  }
}

operation_recovery is normally null. When persisted data could not be fully published back into runtime state, it contains operation, phase, persisted, and message; the scheduler remains stopped until the integration reloads or Home Assistant restarts. Runtime zone_runtime, Room Assist, and Comfort projections are suppressed while a temperature or recovery block is active.

temperature_unit identifies the unit of the stored Velair values. home_assistant_temperature_unit is the unit currently detected from Home Assistant. Both are informational and cannot be changed through Velair. When they differ, temperature_migration.required is true, operational_status is temperature_migration_required, automatic scheduling and Velair write operations remain blocked, and the frontend must ask the user to migrate the stored values.

There is one upgrade exception. A response with temperature_migration.reason equal to legacy_celsius_upgrade_reset_required identifies published Celsius-only data loaded while Home Assistant uses Fahrenheit. The frontend must offer velair/reset_data, not velair/resolve_temperature_migration. Reset replaces the legacy model atomically with defaults in Home Assistant's current unit. Subsequent unit changes use home_assistant_unit_changed and the explicit migration command below.

Resolve Temperature Migration

await hass.connection.sendMessagePromise({
  type: "velair/resolve_temperature_migration",
  source_unit: "°C",
  migration_id: crypto.randomUUID(),
  expected_revision: 3,
});

Available only while the stored unit differs from Home Assistant. The source must match temperature_migration.source_unit; the target is always temperature_migration.target_unit. The command converts all thermal configuration and learning data, persists it atomically, dismisses the notification, starts the scheduler, and returns the full response. The migration id makes exact retries harmless and expected_revision rejects stale clients.

next_event is the scheduler's earliest due action. next_events is the UI-oriented list of the next visible event per managed climate, sorted by apply time; preconditioning events include target_when so the panel can show both the early start and the comfort target time.

Preconditioned native ranges remain an exclusive target union: serialized events include both target_temp_low and target_temp_high and leave temperature null. Their diagnostics expose target_kind: "range", the effective target_boundary (low or high), and boundary_temperature. Scalar events retain target_kind: "scalar" and target_boundary: "temperature".

preconditioning_learning is local runtime/storage status used by the panel. It can be included explicitly in portable exports and is never sent outside Home Assistant by Velair. Direction statuses are learning, ready, or unsupported when the climate does not report a compatible HVAC mode. A direction is ready when it has at least 5 complete local samples; before that, Adaptive predictions use the initial event-specific model.

The actual lead is calculated per future event from the current temperature delta and the selected local model source (initial_model or history).

Stored observations are trimmed per climate direction. learning_history_size limits useful complete and partial samples, while only the 10 newest invalid diagnostic samples are retained separately. Invalid samples cannot evict useful learning history. Heat and cool keep separate local histories, so seasonal cooling samples cannot evict heating samples, and heating samples cannot evict cooling samples.

Range observations use the effective boundary in the existing target_temp field and may add target_temp_low, target_temp_high, and target_boundary. These fields are optional so existing stored and portable observations remain valid. Temperature migration converts both optional boundaries as absolute temperatures.

comfort is the local runtime Environmental Comfort assessment. It contains the human environmental condition, independent CO2 air_quality, data_quality, data_issues, and raw metric payloads. Opening or refreshing the panel does not emit comfort automation events.

Read Schedule State

await hass.connection.sendMessagePromise({
  type: "velair/get_schedule",
});

When a compatible provider is detected, the response also includes external_execution.systems with provider capabilities and eligible entity IDs, plus external_execution.zones with the configured provider, availability, and nullable runtime publication. Publication is null until Velair attempts one in the current runtime; an attempt can be publishing, published, or failed. published means the provider service accepted the call. It is not a hardware verification claim. Availability is reported separately. Selected registered providers retain their name and capabilities if they later become unavailable; unknown persisted provider keys remain safe and unavailable. Publication state is not persisted, read back, retried, or polled.

Schedule capabilities are provider-neutral: supports_profile_schedules, supported_actions, supported_hvac_modes, supported_target_types, supported_option_fields, max_switchpoints_per_day, and time_step_minutes. Providers can also declare implicit_midnight_change_counts_toward_limit when the continuity switchpoint Velair adds at 00:00 consumes one of the daily changes. Clients must derive controller conditions from these fields rather than branching on a provider key. The ramses_cc provider declares set_temperature, heat, scalar, and no supported climate option fields.

Set Zone Execution Ownership

await hass.connection.sendMessagePromise({
  type: "velair/set_zone_execution",
  entity_id: "climate.living_room",
  provider: "ramses_cc",
});

Both entity_id and provider are required. Set provider explicitly to null to return the zone to local Velair execution. External ownership is persisted before publication and remains external if publication fails. While external, all Velair climate actions are rejected. The scheduler resolves and publishes the effective full week: Default for normal Profile behavior, or the Profile schedule for schedule behavior. Profile pause behavior is rejected for external zones. Profile and Mode selection is persisted before publication; a provider failure does not roll the selection back. Re-selecting Default, a Profile, or a Mode explicitly republishes the effective week when runtime publication evidence is failed or absent; no background retry is implied. The result is the full schedule response.

Diagnostics

Diagnostics uses a separate read-only snapshot so opening the view cannot change scheduler state. History contains at most 100 sanitized runtime events; the category selection is persisted, but the events themselves are cleared whenever Velair or Home Assistant restarts.

Read Diagnostics

const diagnostics = await hass.connection.sendMessagePromise({
  type: "velair/get_diagnostics",
});

The request has no fields beyond type. The result has this top-level schema:

interface DiagnosticsSnapshot {
  generated_at: string;
  history_limit: number;
  history_policy: {
    categories: Record<DiagnosticsCategory, boolean>;
    runtime_only: true;
    cleared_on_restart: true;
  };
  overall: {
    status: "ok" | "warning" | "error";
    scheduler_mode: string;
    scheduler_status: string;
    unit_counts: { ok: number; warning: number; error: number };
    issues: Array<Record<string, unknown>>;
  };
  units: Record<string, Record<string, unknown>>;
  history: Array<Record<string, unknown>>;
}

type DiagnosticsCategory =
  | "control"
  | "room_assist"
  | "preconditioning"
  | "comfort"
  | "delivery"
  | "availability";

The command returns not_loaded when the integration runtime is unavailable.

Export Diagnostics

const report = await hass.connection.sendMessagePromise({
  type: "velair/export_diagnostics",
  redact_entity_ids: true,
});

redact_entity_ids is an optional boolean and defaults to true. The result is { privacy, diagnostics }: diagnostics has the snapshot schema above and privacy states whether entity IDs were replaced, confirms that operational Profile, Mode, and pause identifiers are always removed, and reminds callers that history is runtime-only and the report must be reviewed before sharing. Setting redact_entity_ids to false keeps local entity IDs but does not disable operational-identifier redaction. The command returns not_loaded if Velair is unavailable; a non-boolean option is rejected by WebSocket schema validation.

Configure Diagnostics History

const diagnostics = await hass.connection.sendMessagePromise({
  type: "velair/update_diagnostics_history",
  enabled_categories: ["control", "room_assist", "delivery"],
});

enabled_categories is a required list containing only the six DiagnosticsCategory values above. The exact enabled set is persisted in the entry-specific diagnostics policy store. Events belonging to a category that is disabled are immediately removed from the in-memory history, and future events in that category are not retained. Current health calculation and automation events are unaffected. The result is the refreshed DiagnosticsSnapshot. Invalid category names or a missing list fail WebSocket schema validation; not_loaded means the integration is unavailable. A policy storage failure is returned as a command failure and leaves the previous in-memory policy active.

Clear Diagnostics History

const diagnostics = await hass.connection.sendMessagePromise({
  type: "velair/clear_diagnostics_history",
});

The request has no additional fields. It clears only the bounded in-memory event list and returns the refreshed DiagnosticsSnapshot. It does not change enabled categories, current health evidence, schedules, or climate control. The command returns not_loaded when Velair is unavailable.

Subscribe To Diagnostics

const unsubscribe = await hass.connection.subscribeMessage(
  (message) => {
    if (message.loaded) {
      renderDiagnostics(message.diagnostics);
    }
  },
  { type: "velair/subscribe_diagnostics" },
);

The request has no additional fields. Successful registration is acknowledged first, followed immediately by an event shaped as { loaded: true, diagnostics: DiagnosticsSnapshot }. Later lightweight diagnostic revisions use the same event shape. If the integration unloads, the subscription receives { loaded: false }; if it is already unavailable when subscribing, registration returns not_loaded.

Subscribe To Updates

const unsubscribe = await hass.connection.subscribeMessage(
  (message) => {
    if (message.loaded && message.schedule) {
      // Re-render with message.schedule.
    }
  },
  {
    type: "velair/subscribe_updates",
  },
);

The integration sends an event whenever scheduler state changes. The event contains the full schedule response under schedule.

Set Daily Schedule

await hass.connection.sendMessagePromise({
  type: "velair/set_daily_schedule",
  entity_id: "climate.living_room",
  weekday: "monday",
  blocks: [
    {
      start: "06:00",
      action: "set_temperature",
      target_temp_low: 20,
      target_temp_high: 24,
      hvac_mode: "heat_cool",
      fan_mode: "quiet",
      preset_mode: "eco"
    },
    { start: "23:30", action: "turn_off" }
  ],
});

If action is omitted, the backend treats the block as set_temperature for compatibility with older schedules.

Temperature blocks contain either temperature, or the complete target_temp_low and target_temp_high pair. The two representations are mutually exclusive. Blocks may also include optional climate settings: fan_mode, preset_mode, swing_mode, swing_horizontal_mode, and humidity. The scheduler filters these fields against the target climate capabilities before persisting or applying them. Unsupported fields are dropped; turn_off blocks never keep target or optional climate settings.

Copy Day Schedule

await hass.connection.sendMessagePromise({
  type: "velair/copy_day_schedule",
  entity_id: "climate.living_room",
  source_weekday: "monday",
  target_weekdays: ["tuesday", "wednesday"],
});

Clear Schedule

Clear one weekday:

await hass.connection.sendMessagePromise({
  type: "velair/clear_schedule",
  entity_id: "climate.living_room",
  weekday: "monday",
});

Clear all weekdays for one zone:

await hass.connection.sendMessagePromise({
  type: "velair/clear_schedule",
  entity_id: "climate.living_room",
});

Templates

Create a template:

await hass.connection.sendMessagePromise({
  type: "velair/set_schedule_template",
  name: "Evening",
  blocks: [
    {
      start: "18:00",
      action: "set_temperature",
      temperature: 21,
      hvac_mode: "heat",
      fan_mode: "quiet"
    },
    { start: "23:00", action: "set_temperature", temperature: 17 }
  ],
});

Update a template by passing its key:

await hass.connection.sendMessagePromise({
  type: "velair/set_schedule_template",
  key: "evening",
  name: "Evening",
  blocks: [
    { start: "18:00", action: "set_temperature", temperature: 20 }
  ],
});

Delete a template:

await hass.connection.sendMessagePromise({
  type: "velair/delete_schedule_template",
  key: "evening",
});

Settings

await hass.connection.sendMessagePromise({
  type: "velair/update_settings",
  first_weekday: "sunday",
  zone_order: ["climate.living_room"],
  apply_active_schedule_on_startup: true
});

Templates are capability-neutral storage. They can contain optional climate settings from any managed climate. Filtering happens later when a template is applied to one concrete climate schedule.

External-change Policy

await hass.connection.sendMessagePromise({
  type: "velair/update_external_change_policy",
  entity_id: "climate.living_room",
  policy: "for_duration",
  duration_minutes: 120,
});

entity_id is required and must be managed. policy is required and accepts keep_automatic, until_next_block, for_duration, or until_resumed. duration_minutes is optional, is meaningful for for_duration, and must be an integer from 1 through 10080. The policy is the default for future external changes; updating it does not alter an already active Manual adjustment. The result is the full schedule response.

Errors are not_loaded, temperature_migration_required, operation_in_progress, or invalid_external_change_policy; malformed fields can also fail WebSocket schema validation.

Enter Manual Adjustment

await hass.connection.sendMessagePromise({
  type: "velair/enter_manual_adjustment",
  entity_id: "climate.living_room",
});

The only parameter is the required managed entity_id. Velair captures the live HVAC mode and scalar or native-range target, then starts a persisted Manual adjustment using that zone's saved policy. If the saved policy is keep_automatic, explicit entry uses until_resumed without changing the saved default. The result is the full schedule response.

Errors are not_loaded, temperature_migration_required, operation_in_progress, or manual_adjustment_not_allowed. The last includes an unmanaged, unavailable, disabled, already-manual, Profile-paused, otherwise paused, or non-automatic zone.

Resume Automatic Control

await hass.connection.sendMessagePromise({
  type: "velair/resume_automatic_control",
  entity_id: "climate.living_room",
});

The only parameter is the required managed entity_id. Velair removes only its Manual-adjustment pause reason, resolves the current Profile, Mode, schedule, Boost, and remaining pause authority, and applies that intent when allowed. It does not remove independent pause reasons. The result is the full schedule response; calling it when the zone is already automatic is harmless.

Errors are not_loaded, temperature_migration_required, operation_in_progress, or invalid_entity. Malformed entity IDs fail WebSocket schema validation.

Zone Preconditioning

await hass.connection.sendMessagePromise({
  type: "velair/update_zone_preconditioning",
  entity_id: "climate.living_room",
  preconditioning: {
    enabled: true,
    max_lead_minutes: 1440,
    minimum_delta_temperature: 0.3,
    min_start_minutes: 10,
    fallback_minutes_per_degree: 25,
    room_temperature_entity_id: "sensor.living_room_temperature",
    room_sensor_assist_enabled: true,
    room_sensor_assist_deadband: 0.3,
    room_sensor_assist_max_delta: 2.0
  }
});

When preconditioning moves the apply time earlier than the visible schedule block time, serialized events keep when as the apply time and include target_when as the comfort target time. These events also include preconditioning_diagnostics, a runtime-only calculation breakdown with selected sample counts, complete estimate, partial floor, combined estimate, rounded estimate, final lead, model source, and limit flags. The frontend uses this object for optional calculation details instead of recalculating the prediction.

Preconditioning is adaptive. The scheduler predicts a lead for each concrete future event, using an initial model while learning and switching to similar local history after enough complete samples exist.

Outdoor temperature context is optional and local. In the Preconditioning tab, outdoor_temperature_entity_id is selected through a sensor dropdown that lists local sensor.* temperature entities. Velair reads the selected sensor's numeric state, stores it with learning samples, and uses it only to compare similar preconditioning samples once enough history exists. It does not call external weather services and does not apply fixed weather-based adjustments to the initial model.

Room temperature sensor support is optional and local. In the Room Assist tab, room_temperature_entity_id is selected through a sensor dropdown that lists local sensor.* temperature entities. Selecting a sensor stores the configuration, but Velair uses it as the effective room temperature only when room_sensor_assist_enabled is true. In that mode Velair can temporarily adjust a scalar target or move a complete native range while the real scheduled target remains unchanged. For fixed scalar heat and cool, a non-zero room_sensor_assist_deadband defines a runtime hysteresis cycle between the scheduled target minus and plus that value; room_sensor_assist_max_delta caps correction relative to the active edge. A zero deadband preserves legacy signed correction. Scalar automatic modes retain their neutral deadband, while native ranges retain their stable holding margin. Scalar non-driving targets are kept on the safe side of the scheduled target, and a valid native-range holding band remains stable until the external room leaves it or its active target changes. room_sensor_assist_debounce_seconds controls how many seconds Velair waits after relevant state changes before recalculating the assisted target. Room Sensor Assist can run on normal scheduled blocks and can also provide the temperature source for Adaptive Preconditioning while it is enabled.

room_sensor_assist in the schedule response is runtime-only status. It is derived from Home Assistant state and scheduler state when the response is built; it is not persisted as history.

When the thermostat's physical minimum or maximum changes the target that Room Assist actually applied, limited_by is minimum or maximum and limit_temperature identifies that boundary. Scalar status then includes requested_temperature; native ranges instead include requested_target_temp_low and requested_target_temp_high. These optional fields are null when the last applied runtime target was not limited.

For scalar targets, scheduled_target_guard is cooling_floor or heating_ceiling when the scheduled target prevented a non-driving calculation from crossing onto the demanding side. calculated_temperature contains the step-aligned candidate before that protection, while applied_temperature remains the target actually sent. Both fields are optional and runtime-only; clients must treat their absence as an unguarded or older compatible payload.

For scalar targets, status can also include pre_step_temperature together with target_temp_step when alignment to the climate's published step changed the setpoint that was actually committed. pre_step_temperature is the candidate after scheduled-target protection and min/max clamping, immediately before alignment. The pair is absent for exact-step results, native ranges, results changed by a final physical-limit clamp, and older compatible payloads. Diagnostics copies these live status fields, but they are not persisted or included in automation events or Home Assistant sensor attributes.

Fixed scalar hysteresis status can additionally include hysteresis_phase (towards_lower or towards_upper), hysteresis_target, deadband_low, and deadband_high. These fields are optional, runtime-only, and absent for the zero-deadband, scalar automatic, and native-range paths. The phase is not persisted and resets on an active block, target, or HVAC-mode change, when assistance is cleared, and when Velair or Home Assistant reloads.

See Adaptive preconditioning for the full learning lifecycle, input/output examples, prediction rules, storage behavior, and known limitations. See Room Assist for the room sensor assistance lifecycle, target calculation, runtime status, restoration behavior, and events.

Zone Comfort

await hass.connection.sendMessagePromise({
  type: "velair/update_zone_comfort",
  entity_id: "climate.living_room",
  comfort: {
    enabled: true,
    temperature_entity_id: "sensor.living_room_temperature",
    humidity_enabled: true,
    humidity_entity_id: "sensor.living_room_humidity",
    co2_entity_id: "sensor.living_room_co2",
    temperature_min: 20,
    temperature_max: 24,
    humidity_min: 40,
    humidity_max: 60,
    co2_attention: 1000,
    co2_poor: 1500,
    stale_after_minutes: 120
  }
});

Comfort settings are per managed climate. The scheduler only listens to comfort-related entities for climates where comfort.enabled is true. See Environmental Comfort internals for source selection, assessment calculation, runtime listener behavior, and event payloads.

Climate Profiles

Create or replace a complete profile definition. Omitting key creates a new stable key; including it updates that profile. Profile zones are sparse: omitted zones keep their default schedule.

await hass.connection.sendMessagePromise({
  type: "velair/set_profile",
  profile: {
    name: "Away",
    icon: "mdi:home-export-outline",
    color: "#546e7a",
    description: "Lower demand while nobody is home",
    zones: {
      "climate.living_room": {
        behavior: "pause",
        action: "turn_off"
      }
    }
  }
});

The response is the full schedule response plus profile_id, containing the created or updated key. Delete a profile with:

await hass.connection.sendMessagePromise({
  type: "velair/delete_profile",
  key: "away"
});

Deleting an active Profile removes only that ID from the active set; its zones return to Default and other active Profiles remain. Activate one Profile as a Manual singleton, or empty the active set with null for compatibility, using:

await hass.connection.sendMessagePromise({
  type: "velair/activate_profile",
  profile_id: "away"
});

Activation replaces the complete active set with that Profile. Zones present in the previous set but not covered by the selected Profile resolve immediately to their Default schedules. It applies the current effective schedule, cancels Boost on affected zones, and preserves global or per-zone manual pauses. Direct activation clears active_mode_id, so the native selector reports Manual. New automations should use velair.deactivate_profile explicitly to return to default schedules.

Create or update one Mode with:

await hass.connection.sendMessagePromise({
  type: "velair/set_mode",
  mode: {
    name: "Away",
    profile_ids: ["away"]
  }
});

Omit key when creating a mode; include the stable returned key when updating one. Names are trimmed, limited to 255 characters, case-insensitively unique, cannot contain control characters, and cannot use the built-in names Default or Manual, translated built-in labels such as Predeterminado, nor the Home Assistant states unknown and unavailable. Every Mode must provide one or more unique existing IDs in profile_ids. Selected Profiles must not explicitly configure the same zone; conflicting compositions are rejected. Delete a Mode with:

await hass.connection.sendMessagePromise({
  type: "velair/delete_mode",
  key: "away-mode"
});

The schedule response contains modes and active_mode_id. Automations select a configured value through Home Assistant's standard select.select_option action on select.velair_mode.

The Velair panel changes the active Mode through a structured WebSocket command so it does not depend on the user-editable Home Assistant entity ID:

await hass.connection.sendMessagePromise({
  type: "velair/select_mode",
  selection: { kind: "custom", key: "away-mode" }
});

Custom selections use the stable mode key. The built-in selections are { kind: "default" } and { kind: "manual" }. Panel custom selections report panel as their profile-change source; changes made through the native select entity continue to report select.

Reset Zone Preconditioning Settings

await hass.connection.sendMessagePromise({
  type: "velair/reset_zone_preconditioning_settings",
  entity_id: "climate.living_room"
});

Restores default tuning parameters for one managed climate. The current enabled state, schedules, and all heat and cool learning samples are preserved.

Reset Zone Preconditioning Learning

await hass.connection.sendMessagePromise({
  type: "velair/reset_zone_preconditioning_learning",
  entity_id: "climate.living_room",
  direction: "heat"
});

Deletes local adaptive preconditioning observations for one managed climate direction. Valid directions are heat and cool. Schedule blocks, preconditioning settings, and the other direction's observations are kept.

Export Data

await hass.connection.sendMessagePromise({
  type: "velair/export_data",
  sections: ["zones", "templates", "settings", "preconditioning_learning", "profiles", "modes"],
});

Returns a versioned portable JSON payload:

{
  "format": "velair_portable_data",
  "model_version": 8,
  "temperature_unit": "°C",
  "exported_at": "2026-05-25T00:00:00+00:00",
  "sections": {}
}

Import Data

await hass.connection.sendMessagePromise({
  type: "velair/import_data",
  payload,
  sections: ["templates"]
});

Selected sections overwrite existing data. Profile and Mode definitions are portable, but the active Profile set and selected Mode are never exported or selected by import. Replacing definitions retains active IDs that still exist and returns to default schedules only when the active set becomes empty.

Portable model v5 payloads add the optional modes section. V4 payloads remain valid. Portable payloads declare temperature_unit; models created before unit metadata existed may omit it; the backend treats those values as Celsius. Portable model v8 adds the independent room_sensor_assist_deadband. When a v7 or older zone omits it, import migrates the legacy minimum_delta_temperature value before any unit conversion. If the source differs from Velair's current Home Assistant unit, selected thermal data is converted before normalization. Managed climates with known limits and target_temp_step are aligned to that exact grid. Standalone template values use safe fallback precision when no common exact device step can be derived. Data for unmatched climate IDs is not applied or used to transform existing local zones.

Export is available while scheduling is blocked by a pending temperature-unit resolution and remains a read-only operation during reset, migration, or import.

The preconditioning_learning section is incremental by climate entity ID: matching managed climates receive the normalized imported history, unknown climate IDs are ignored, and existing history for local climates absent from the file is preserved.

Reset Data

await hass.connection.sendMessagePromise({
  type: "velair/reset_data",
  confirmation: "reset"
});

This deletes all stored Velair data, including schedules, templates, panel preferences, active boosts and pauses, per-zone external-change policies and persisted Manual-adjustment state, Comfort and Room Assist settings, Adaptive Preconditioning settings and learning, and startup behavior. It then recreates unit-aware defaults for the currently managed climates. The separate Diagnostics category policy and runtime log are not schedule data reset by this command. The frontend must ask the user for confirmation before calling this command.

Error Behavior

  • not_loaded: the integration is not loaded.
  • invalid_schedule: a schedule is invalid or targets an unmanaged climate.
  • invalid_template: a template is invalid or unknown.
  • invalid_settings: settings are invalid.
  • invalid_external_change_policy: an external-change policy or target is invalid.
  • manual_adjustment_not_allowed: the requested zone cannot enter Manual adjustment in its current state.
  • invalid_entity: the requested entity is not managed by Velair.
  • invalid_preconditioning: preconditioning settings are invalid or target an unmanaged climate.
  • invalid_comfort: Comfort settings are invalid or target an unmanaged climate.
  • invalid_import: the import file is invalid or incompatible.
  • invalid_temperature_migration: the requested source, revision, or migration state is invalid.
  • operation_in_progress: a reset, migration, or import currently owns the data write guard.
  • operation_recovery_required: data was persisted, but runtime cleanup or option updates failed. The scheduler remains stopped until the integration is reloaded or Home Assistant restarts.
  • temperature_migration_required: thermal writes remain stopped until the stored unit is resolved.
  • temperature_migration_failed: the migration could not be persisted; its write guard was released and scheduling remains governed by the still-stored unit metadata.

Climate actions use blocking Home Assistant calls so explicit invocation failures are observable. Scheduler-owned intentions may complete later through bounded runtime recovery; delayed attempts always resolve current backend state instead of replaying the original service payload. Manual temperature actions remain one-shot and report an unavailable or rejected call to their caller.