ST for Home Assistant is a HACS integration that enables programming Home Assistant automations in Structured Text (IEC 61131-3) - the language used in industrial PLCs (TwinCAT, Siemens, etc.).
Core Concept: ST code is transpiled (not interpreted) to native HA automations, resulting in zero runtime overhead.
┌─────────────────────────────────────────────────────────────────────────────┐
│ ST for Home Assistant │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ ST Code │───▶│ Parser │───▶│ AST │───▶│ Analyzers │ │
│ │ (Editor) │ │ (Chevrotain) │ │ │ │ │ │
│ └─────────────┘ └──────────────┘ └─────────────┘ └─────┬─────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ HA Runtime │◀───│ YAML Output │◀───│ Transpiler │◀───│ Triggers │ │
│ │ (Automation)│ │ │ │ │ │ + Helpers │ │
│ └─────────────┘ └──────────────┘ └─────────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| Component | Technology | Version | Status |
|---|---|---|---|
| Editor | CodeMirror 6 | ^6.0 | ✅ Implemented |
| Parser | Chevrotain | ^11.0 | ✅ Implemented |
| Frontend | TypeScript + Lit | 5.x / 3.x | ✅ Implemented |
| Build | Vite | ^5.0 | ✅ Implemented |
| Backend | Python (HA Custom Component) | 3.11+ | ✅ Implemented |
| HA Integration | HACS | - | ✅ Implemented |
- Data Types: BOOL, INT, DINT, LINT, SINT, REAL, LREAL, STRING, TIME
- Control Structures: IF/ELSIF/ELSE, CASE, FOR, WHILE, REPEAT
- Operators: Arithmetic, comparison, logical (AND, OR, XOR, NOT)
- Built-in Functions: SEL, MUX, LIMIT, MIN, MAX, ABS, SQRT, etc.
- Function Blocks: R_TRIG, F_TRIG, TON, TOF, TP, SR, RS
VAR
motion AT %I* : BOOL := 'binary_sensor.kitchen_motion'; // Input
light AT %Q* : BOOL := 'light.kitchen'; // Output
END_VAR
{mode: restart} // Script execution mode
{throttle: T#1s} // Rate limiting
{debounce: T#500ms} // Debouncing
{trigger} // This variable triggers automation
{no_trigger} // Variable doesn't trigger
{persistent} // Value persists across runs (→ Helper)
{transient} // Only valid during run
{reset_on_restart} // Always use initial value after HA restart
{require_restore} // Error if no stored value exists
ST thinks: "I continuously check"
HA thinks: "I sleep until an event comes"
MUST: Implement Dependency Analysis
- Automatically register all read entity variables as triggers
- Respect
{trigger}/{no_trigger}pragmas - Warning when no triggers detected → program never runs
// CORRECT
trigger:
- platform: state
entity_id: binary_sensor.motion // Auto-generated from AT %I*ST: Variables retain value between cycles
HA: Variables only live milliseconds
MUST: Tiered Storage Strategy
DERIVED→ Entity-bound variables (no helper)PERSISTENT→ Self-reference, FB instances, timers (→ input_* helper)TRANSIENT→ Everything else (HA variables:)
MUST: Namespace Convention
input_number.st_<project>_<program>_<variable>
MUST: Cleanup mechanism for obsolete helpers
Sensor can be: "unavailable", "unknown", "none", ""
→ Jinja error or wrong result
MUST: Generate null-safe templates
{{ states('sensor.temp') | float(default=0.0)
if states('sensor.temp') not in ['unavailable', 'unknown', 'none', '']
else 0.0 }}WHILE without exit → HA frozen
MUST: Automatic iteration limits
repeat:
while:
- "{{ original_condition }}"
- "{{ _safety_counter < 1000 }}" # AUTO-INSERTEDMUST: Compiler warning for WHILE without guaranteed exit
mode: single → Input loss (bad!)
mode: restart → New value takes priority (PLC-like, good!)
MUST: Default mode: restart for all generated scripts
Deploy halfway through + error → Inconsistent state
MUST:
- Backup before deploy
- Rollback on error
- All changes or none
HA error shows: "Error in automation.yaml line 47"
User thinks: "Which ST line is that?"
MUST: Source maps in generated YAML
variables:
_st_source_map:
"action.0.choose.0": { st_line: 7, st_file: "kitchen.st" }HA delay is NOT interruptible
ST timers (TON) are resettable
MUST: Implement timer FBs with timer.* entity + timer.finished event
WRONG: Directly edit automations.yaml
CORRECT: Use HA services
MUST: Deployment exclusively via HA APIs
automation.reloadafter changesinput_number.set_valuefor helpers- WebSocket API for entity creation
WHY:
- File manipulation is fragile (formatting, comments, merges)
- HA cannot track changes
- No rollback possible with direct file changes
- User edits get overwritten
// CORRECT - Via HA Storage API
await hass.callWS({
type: 'config/automation/config',
automation_id: 'st_kitchen',
config: generatedAutomation
});First run: input_datetime is empty/unavailable
→ Template crashes or gives wrong result
MUST: Robust throttle condition with fallback
{# WRONG - crashes with empty helper #}
{{ (now() - states('input_datetime.st_last_run') | as_datetime).total_seconds() > 1 }}
{# CORRECT - with fallback for first run #}
{% set last = states('input_datetime.st_last_run') %}
{% if last in ['unknown', 'unavailable', ''] %}
true
{% else %}
{{ (now() - (last | as_datetime)).total_seconds() > 1 }}
{% endif %}MUST: Initialize helper on deploy if not existing
# WRONG - Anti-pattern in HA!
while True:
check_conditions()
sleep(0.1) # 100ms cycle timeWHY: Blocks HA, performance killer, unnecessary CPU usage
INSTEAD: Event-based triggers from dependency analysis
# WRONG - Every variable as helper
input_number.st_temp_var_1
input_number.st_temp_var_2
input_number.st_loop_counter
# ... 50+ helpers for one programWHY: Clutters HA instance, hard to maintain
INSTEAD: Only PERSISTENT variables as helpers, rest in variables:
# WRONG
script:
st_kitchen_logic:
mode: single # ← Triggers are ignored during run!WHY: Input loss, not PLC-like
INSTEAD: mode: restart (or queued for special cases)
# WRONG
{{ states('sensor.temp') * 2 }} # Crashes on "unavailable"WHY: Jinja errors, wrong results (e.g., "unavailable" * 2)
INSTEAD: Always defensive templates with | float(default=0.0)
// WRONG - Can freeze HA
WHILE NOT sensor DO
// wait...
END_WHILE;
WHY: Blocks automation thread
INSTEAD: Automatic safety counter, max 1000 iterations
// WRONG
const trigger = { entity_id: "binary_sensor.motion" };WHY: Not portable, hard to test
INSTEAD: Extract from AST EntityBinding
# WRONG
async def deploy():
await delete_old_helpers()
await create_new_helpers() # ← Error here = data loss!WHY: Inconsistent state, data loss
INSTEAD: Backup → Changes → Verify → Commit (or Rollback)
// WRONG
await hass.callService('input_number', 'set_value', {...});WHY: Hard to test, API changes break code
INSTEAD: Helper Manager abstraction layer
# WRONG - Never!
with open('/config/automations.yaml', 'w') as f:
yaml.dump(automation, f)WHY:
- Overwrites user comments and formatting
- HA doesn't track changes
- No rollback on error
- Race conditions with HA Core
- Security risk
INSTEAD: HA Storage API / WebSocket services
# WRONG - crashes on first run
{{ (now() - states('input_datetime.x') | as_datetime).total_seconds() }}WHY: input_datetime can be unknown/unavailable
INSTEAD: Always fallback for empty/new helper
| Task | Description | Status |
|---|---|---|
| T-001 | Repository Setup (HACS structure) | ✅ Complete |
| T-002 | CodeMirror 6 ST Editor | ✅ Complete |
| T-003 | Chevrotain Parser | ✅ Complete |
| T-004 | Dependency Analyzer | ✅ Complete |
| T-005 | Storage Analyzer | ✅ Complete |
| T-006 | Archive Gap Analysis | ✅ Complete |
| T-007 | Transpiler Basis | ✅ Complete |
| T-008 | Helper Manager & Deploy | ✅ Complete |
| T-009 | Timer FBs (TON/TOF/TP) | ✅ Complete |
| T-010 | Source Maps & Error Mapping | ✅ Complete |
| T-011 | Restore Policy & Migration | ✅ Complete |
| T-012 | Live Values & Online Mode | ✅ Complete |
| Module | Tests | Status |
|---|---|---|
| Parser | 25 | ✅ Passing |
| Dependency Analyzer | 16 | ✅ Passing |
| Storage Analyzer | 23 | ✅ Passing |
| Transpiler | 15 | ✅ Passing |
| Timer Transpiler | 9 | ✅ Passing |
| Deploy Manager | 2 | ✅ Passing |
| Helper Manager | 2 | ✅ Passing |
| Restore/Migration | 20 | ✅ Passing |
| Online Mode | 10 | ✅ Passing |
| Error Mapping | 10 | ✅ Passing |
| Source Maps | 11 | ✅ Passing |
| Total | 145 | ✅ 100% Passing |
- TypeScript Compilation: ✅ Passing (strict mode)
- ESLint: ✅ Passing (0 errors)
- Bundle Size: 923.60 KB (245.16 KB gzipped)
- Build Time: ~2s
| Risk | Impact | Probability | Mitigation | Status |
|---|---|---|---|---|
| Cycle→Event incorrect | 🔴 High | Medium | Dependency Analyzer + Tests | ✅ Solved |
| State loss without persistence | 🔴 High | High | Storage Analyzer + Helpers | ✅ Solved |
| Timer not interruptible | 🟡 Medium | High | Timer Entity Pattern | ✅ Solved |
| Jinja errors on unavailable | 🟡 Medium | High | Defensive Templates | ✅ Solved |
| Loop blocking | 🔴 High | Medium | Safety Guards | ✅ Solved |
| Deploy inconsistency | 🟡 Medium | Low | Transactional Deploy | ✅ Solved |
| Parser complexity | 🟡 Medium | Medium | Iterative expansion | ✅ Solved |
| File manipulation instead of API | 🔴 High | Low | Only HA Storage API | ✅ Solved |
| Throttle helper empty | 🟡 Medium | High | Fallback in template | ✅ Solved |
| Parser choice wrong | 🟡 Medium | Low | Spike with evaluation | ✅ Solved |
ST_HA_Automation/
├── custom_components/
│ └── st_hass/
│ ├── __init__.py
│ ├── manifest.json
│ ├── config_flow.py
│ ├── const.py
│ ├── strings.json
│ └── frontend/
│ └── st-panel.js (built)
├── frontend/
│ ├── src/
│ │ ├── index.ts
│ │ ├── editor/
│ │ │ ├── st-language.ts
│ │ │ ├── st-theme.ts
│ │ │ ├── st-editor.ts
│ │ │ └── index.ts
│ │ ├── parser/
│ │ │ ├── tokens.ts
│ │ │ ├── lexer.ts
│ │ │ ├── ast.ts
│ │ │ ├── parser.ts
│ │ │ ├── visitor.ts
│ │ │ └── index.ts
│ │ ├── analyzer/
│ │ │ ├── dependency-analyzer.ts
│ │ │ ├── storage-analyzer.ts
│ │ │ ├── trigger-generator.ts
│ │ │ ├── helper-mapping.ts
│ │ │ └── index.ts
│ │ ├── transpiler/
│ │ │ ├── transpiler.ts
│ │ │ ├── action-generator.ts
│ │ │ ├── jinja-generator.ts
│ │ │ ├── timer-transpiler.ts
│ │ │ └── index.ts
│ │ ├── deploy/
│ │ │ ├── deploy-manager.ts
│ │ │ ├── backup-manager.ts
│ │ │ ├── helper-manager.ts
│ │ │ ├── ha-api.ts
│ │ │ └── index.ts
│ │ ├── online/
│ │ │ ├── state-manager.ts
│ │ │ ├── live-decorations.ts
│ │ │ └── index.ts
│ │ ├── restore/
│ │ │ ├── restore-policy.ts
│ │ │ ├── migration-handler.ts
│ │ │ └── index.ts
│ │ ├── error-mapping/
│ │ │ ├── error-mapper.ts
│ │ │ └── index.ts
│ │ ├── sourcemap/
│ │ │ ├── source-map.ts
│ │ │ └── index.ts
│ │ └── panel/
│ │ └── st-panel.ts
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── docs/
│ ├── 00_Project_Overview.md (this file)
│ ├── PRD_ST_HomeAssistant.md
│ ├── agents/
│ │ ├── agents.md
│ │ └── tasks.md
│ └── archive/
│ ├── 01_Repository_Setup.md
│ ├── 02_CodeMirror_Spike.md
│ ├── 03_Parser_Spike.md
│ └── ... (completed task documentation)
├── hacs.json
├── README.md
└── LICENSE
- Chevrotain: https://chevrotain.io/
- CodeMirror 6: https://codemirror.net/
- HA WebSocket API: https://developers.home-assistant.io/docs/api/websocket
- HACS: https://hacs.xyz/docs/publish/start
- IEC 61131-3: Wikipedia / Beckhoff Infosys
- CAFE (Reference): https://github.com/FezVrasta/cafe-hass
- Repository cloned
- Node.js 20+ installed
-
cd frontend && npm installexecuted -
npm run buildsuccessful - Home Assistant development environment (optional)
- 00_Project_Overview.md read
- MUST-DO's and MUST-NOT-DO's understood
- PRD_ST_HomeAssistant.md reviewed for detailed specifications
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation:
/docsfolder
Last updated: January 2026