{"id":51230950,"url":"https://github.com/timzifer/shutter_engine","last_synced_at":"2026-06-28T16:02:01.331Z","repository":{"id":364943243,"uuid":"1269782253","full_name":"timzifer/shutter_engine","owner":"timzifer","description":"Home Assistant custom integration (HACS) for centralized, resolver-based roller shutter (Rollladen) control — replaces manual automations with one per-cover state machine.","archived":false,"fork":false,"pushed_at":"2026-06-25T05:06:45.000Z","size":1349,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-25T06:19:39.787Z","etag":null,"topics":["blinds","cover","custom-component","hacs","hacs-integration","home-assistant","homeassistant","integration","rollladen","shutter","smart-home"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/timzifer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-15T05:06:02.000Z","updated_at":"2026-06-25T05:06:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/timzifer/shutter_engine","commit_stats":null,"previous_names":["timzifer/shutter_engine"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/timzifer/shutter_engine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timzifer%2Fshutter_engine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timzifer%2Fshutter_engine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timzifer%2Fshutter_engine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timzifer%2Fshutter_engine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/timzifer","download_url":"https://codeload.github.com/timzifer/shutter_engine/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timzifer%2Fshutter_engine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34894560,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blinds","cover","custom-component","hacs","hacs-integration","home-assistant","homeassistant","integration","rollladen","shutter","smart-home"],"created_at":"2026-06-28T16:02:00.620Z","updated_at":"2026-06-28T16:02:01.325Z","avatar_url":"https://github.com/timzifer.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shutter Engine\n\nA Home Assistant **custom component (HACS)** that replaces scattered shutter\n(Rollladen) automations with one central, room-based, resolver-driven state\nmachine.\n\nInstead of a pile of YAML automations, all modes and functions are merely\n*inputs*. A per-cover **resolver** (running inside a `DataUpdateCoordinator`)\nderives exactly **one** target position for every cover on every relevant\ntrigger.\n\n## Architecture\n\nThe core principle is a strict separation between:\n\n- **Drivers** — propose a target position. An ordered priority ladder where the\n  **first match wins**.\n- **Constraints** — applied *afterwards*; they modify or veto the result\n  (e.g. \"do not move while frozen\", \"clamp to a ventilation slot\").\n\nThe decision logic lives in [`custom_components/shutter_engine/engine`](custom_components/shutter_engine/engine),\nwhich is **completely independent of Home Assistant** and fully unit-tested. The\nHome Assistant layer (coordinator, entities, config flow) only feeds resolved\ninputs into the engine.\n\n### Priority ladder (drivers)\n\n1. **Fire / smoke (escape route)** → open participating covers to 100 %.\n   *Breaks the frost and minimum-interval constraints — life safety before motor\n   protection.*\n2. **Burglary / security** → default: no action; optional fixed position.\n3. **Storm** → safe position (only for wind-protected covers).\n4. **Lock** → hold the current position, automation suspended.\n5. **Night / morning** → time-window gated brightness/relative trigger. The night\n   phase is **latched and persisted**: once it fires it keeps the covers closed\n   across the window end, midnight and restarts until the morning trigger releases\n   it (so e.g. eco mode can't reopen them mid-night, and they still close in\n   summer even when no dusk falls inside the window). Without a configured morning\n   window there is no defined reopen point, so the latch is disabled and night\n   stays momentary (the day mode reopens after the night window).\n6. **Sun / eco / heat protection** → sun funnel + brightness (+ temperature).\n7. **Default** → hold the last position.\n\n### Constraints (applied after the ladder)\n\n- **Frost** → block all movement (priority over storm). Only for frost-protected\n  covers.\n- **Lock-out protection** (window contact):\n  - *open* → absolute lock, drive/stay fully open;\n  - *tilted* → clamp \"close\" commands to a ventilation slot.\n- **Minimum movement interval** → suppress command spam / relay wear.\n\n\u003e Deliberate trade-off: **frost beats storm.** A frozen shutter must not move,\n\u003e even in a storm, to protect the motor.\n\n### Manual overrides\n\nThe engine only issues **momentary** commands and does not continuously track a\ncover's physical position. Comfort drivers (night, morning, sun / eco / heat\nprotection) act **once when their decision changes** — if you move a cover by\nhand afterwards, the automation does **not** drive it back; it only acts again\non the next decision change. Safety drivers (fire, storm) and the lock-out\nconstraints keep **enforcing** their target, so they self-correct a manual\nchange. Frost continues to block movement. Use the **lock / disable** controls\nto suspend automation entirely.\n\n## Data model (inheritance)\n\nConfiguration is layered `Hub → Ruleset → Controller → Window`. Every tunable\nvalue may be set on any level; the **deepest set value wins**. The shade type\n(`venetian` / `roller_shutter` / `standard`) seeds protection participation and\nhardware capabilities, each individually overridable.\n\n- **Ruleset** — a reusable behaviour bundle: target positions per day mode,\n  brightness/temperature thresholds and the night/morning time windows. Several\n  rulesets can exist side by side.\n- **Controller** — bound to one Home Assistant area; references exactly **one**\n  ruleset and adds the heating/temperature entities. Exposes the runtime\n  controls (mode select, lock/night/morning/holiday switches, status sensor).\n- **Window** — a controllable surface: picks its controller and **one or more**\n  cover entities, then adds the sun funnel (azimuth/elevation), the escape-route\n  flag and any per-window overrides. Grouping several covers (e.g. a window front\n  in the same room) saves configuration; they share the surface configuration but\n  are each resolved individually from their own position and runtime state.\n\n## Installation\n\n### HACS (recommended)\n\n1. Add this repository as a custom repository (category: *Integration*).\n2. Install **Shutter Engine**.\n3. Restart Home Assistant.\n4. Add the integration via **Settings → Devices \u0026 Services → Add Integration**.\n\n### Manual\n\nCopy `custom_components/shutter_engine` into your Home Assistant\n`config/custom_components` directory and restart.\n\n## Configuration\n\nThe **config flow** sets up the global (hub) entities: sun, weather, workday,\nwind, frost, fire and burglary sensors. They can be changed later from the\nintegration's **Configure** (options) dialog.\n\nEverything else is added as individual **config subentries** from the\nintegration page — each with its own small form and its own device:\n\n1. **Add ruleset** — define the behaviour (positions, thresholds, time windows).\n2. **Add controller** — pick an area and the ruleset that drives it.\n3. **Add window** — pick one or more covers, their controller, the sun funnel\n   and the escape-route flag.\n\nEach subentry can be reconfigured or deleted independently. See\n[`examples/subentries.json`](examples/subentries.json) for the stored data\nshape of each subentry type.\n\n### Dynamic venetian slat tracking\n\nVenetian blinds (Raffstore) can hold their shade position while continuously\nre-angling their slats to track the sun: low sun closes the slats to cut off the\nnear-horizontal beam, high sun opens them to admit more diffuse daylight. A\nconfigurable **dead band** (`sun_tracking_deadband`, degrees) suppresses\nmicro-movements so the slats only re-adjust when the change is worth a motor\nmove. Tracking is on by default for the `venetian` shade type and overridable\nper cover; the statically configured tilt is used as a fallback when no sun data\nis available.\n\n### Entities exposed per controller\n\n- `select.\u003ccontroller\u003e_mode` — off / sun protection / eco / heat protection\n- `switch.\u003ccontroller\u003e_lock` — suspend automation\n- `switch.\u003ccontroller\u003e_night` / `switch.\u003ccontroller\u003e_morning` — time functions\n- `switch.\u003ccontroller\u003e_holiday` — presence simulation (randomized offsets)\n- `sensor.\u003ccontroller\u003e_status` — per-cover diagnostic text (diagnostic category)\n- `sensor.\u003ccontroller\u003e_debug` — diagnostic decision dump (disabled by default):\n  per cover the winning rule (`selected_driver`), the final reason and the\n  constraints that took effect\n\nBoth controller sensors live in the device's **Diagnostics** section. Each\n**window** additionally exposes `sensor.\u003cwindow\u003e_status` (also diagnostic),\nsummarizing its covers with the resolved decision per cover entity.\n\nLegacy devices left over from before the ruleset/controller/window split (shown\nin Home Assistant as \"devices not assigned to a subentry\") are removed\nautomatically on setup.\n\n## Development\n\n```bash\npip install -r requirements_test.txt\npytest           # run the engine test suite\nruff check .     # lint\nruff format .    # format\n```\n\nThe engine tests run without a Home Assistant installation. CI additionally runs\n`hassfest` and HACS validation (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)).\n\n### Visual behaviour examples\n\n[`docs/example.md`](docs/example.md) documents how the controllers/drivers behave\nover a full day with worked, \"real\" examples: time-series simulations of sun\nposition, indoor temperature, manual lock, fire and burglary, each rendered as a\nchart of the inputs versus the resolved cover position. The charts are\n(re)generated by `pytest tests/test_visual_scenarios.py` (requires `matplotlib`,\nalready in `requirements_test.txt`).\n\n## Roadmap\n\n- Phase-2 time-based position emulation for on/off-only actors.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimzifer%2Fshutter_engine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftimzifer%2Fshutter_engine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimzifer%2Fshutter_engine/lists"}