Files
home-assistant-controller/docs/superpowers/specs/2026-08-20-module-split-design.md
T
2026-08-20 14:14:44 +03:00

131 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Module split & warning cleanup for `MyLib.hs`
Date: 2026-08-20
Status: Approved (pending spec review)
## Goal
Split `src/MyLib.hs` (352 lines, monolithic) into vertically-separated modules and fix all compiler warnings, committing between every change.
Two concerns were named upfront:
1. AFRP (Mealy) internals — generic FRP machinery.
2. Home Assistant business logic.
A third concern emerged during exploration:
3. IO runtime — websocket client, effect interpreter, `defaultMain`.
## Module layout
Three vertical modules with hierarchical naming. `MyLib` is removed; `app/Main.hs` and the cabal `exposed-modules` are updated.
| Module | Concern | Depends on |
|---|---|---|
| `AFRP` | Generic Mealy/Event machinery — no HA knowledge | base, time |
| `HomeAssistant.Controller` | HA domain: effect type, entity parsers, controllers | `AFRP`, aeson, lens |
| `HomeAssistant.Runtime` | IO interpreter, websocket client, `defaultMain` | both, websockets |
### Naming rationale
`AFRP` lives at the top level (not `HomeAssistant.AFRP`) because the code is generic FRP machinery with zero HA references; nesting it under `HomeAssistant.*` would misrepresent it. It stays in-project per the user's decision, so a short top-level name is fine.
### Future direction (deferred)
When real controllers exist, split `HomeAssistant.Controller` further into sub-modules:
- `HomeAssistant.Controller.Effect``HASSEff`, `Service`, `HASS`, `callService`
- `HomeAssistant.Controller.Entity` — entity parsers
- `HomeAssistant.Controller` (or `.Controllers`) — actual controllers (`lightController`, etc.)
Deferred now because there's only one proof-of-concept controller.
## Module contents
### `AFRP` (from MyLib.hs lines 2991, 99173)
Exports:
- `Mealy(..)`, `eff`
- `Event(..)`, `hold`, `events`, `switch`
- accumulators: `preMapAccum`, `preMapAccumUTCTime`, `mapAccum`, `mapAccumUTCTime`
- `changes`, `whenA`, `filterA`, `thenA`, `(>>|)`, `toEvent`
- instances come along with `Mealy(..)`
### `HomeAssistant.Controller` (from lines 3242, 5053, 175256)
Exports:
- `Service(..)`, `HASSEff(..)`, `HASS`, `callService`
- entity helpers: `entityChangeEvent`, `entityChangeEvent'`, `entityRead`, `entityRead'`, `entityBool`, `entityBool'`
- domain types/values: `Ruuvi(..)`, `ruuvi`, `ruuviTemperatures`, `ruuviPressures`, `DoorState(..)`, `door`, `light`, `lightController`
### `HomeAssistant.Runtime` (from lines 259268, 270352)
Exports:
- `defaultMain`, `app`, `step`
- `CallIdGen`, `mkCallIdGen`, `hassEval`, `dryRunHassEval`, `receiveJSON`, `wsCallService`
## Warning fixes
Per-warning, applied during the split-then-fix commit sequence:
| Warning (line) | Fix |
|---|---|
| `numericDirection` (190) + `Direction`/`Increase`/`Decrease`/`Steady` (187) — unused, no sig | **Delete.** True experiment; used nowhere. |
| `entityChangeEvent` (224), `entityRead'` (236), `entityRead` (249) — unused top-binds | **Export** from `HomeAssistant.Controller`. Useful Either/Event-returning entity helpers the others build on; the split's export lists make them used. |
| `isEntity` (227) — unused local in `entityChangeEvent` | **Delete.** `entityChangeEvent` delegates to `entityChangeEvent' >>> toEvent`; its `isEntity` is a dead duplicate. |
| `state` (251) in `entityRead`, `state` (256) in `entityBool` — unused locals | **Delete.** Both functions delegate to their `'`-primed versions; the `where` clauses are dead duplicates. |
| `toBool` (244) — non-exhaustive (`"on"`/`"off"` only) | **Add catch-all `_ -> False`.** Conservative: treat unknown HA state as off rather than crashing. |
| `wsCallService` (327) + `conn` (346) — unused, paired | See option C below. |
### `wsCallService` / `conn` — option C (chosen)
The commented-out `wsCallService conn ...` call on line 350 made `conn` unused. Rather than uncomment (behavior change) or paper over with `_conn` (dishonest), expose both behaviors as named, exported functions:
```haskell
hassEval :: CallIdGen -> WS.Connection -> HASSEff a -> IO a
hassEval gen conn = \case
CallService x -> do
callId <- generateCallId gen
wsCallService conn callId (serviceDomain x) (serviceName x) (serviceTarget x)
Pure a -> pure a
dryRunHassEval :: CallIdGen -> HASSEff a -> IO a
dryRunHassEval gen = \case
CallService x -> do
callId <- generateCallId gen
print (callId, x)
Pure a -> pure a
```
Both `conn` and `wsCallService` become used by the real `hassEval`; the current print-only behavior lives in `dryRunHassEval`. `app` wires up **`dryRunHassEval`** to preserve current runtime behavior — flipping to real `hassEval` later is a one-word change.
## Commit sequence
Two commits, each building cleanly:
1. **`Split MyLib into AFRP, Controller, Runtime`**
- Mechanical move of code into `src/AFRP.hs`, `src/HomeAssistant/Controller.hs`, `src/HomeAssistant/Runtime.hs`.
- Proper export lists (which naturally exports the "useful but unused" entity helpers, resolving those three unused-binding warnings).
- Delete `src/MyLib.hs`.
- Update `app/Main.hs` import (`MyLib` -> `HomeAssistant.Runtime`).
- Update cabal `exposed-modules`.
- Build still warns on remaining items.
2. **`Fix warnings`**
- Delete `numericDirection`/`Direction` and dead `where` clauses (`isEntity` in `entityChangeEvent`, `state` in `entityRead`/`entityBool`).
- Add `_ -> False` catch-all to `toBool`.
- Implement real `hassEval` (uncommented `wsCallService`) + add `dryRunHassEval`; export both from `HomeAssistant.Runtime`.
- Wire `app` to `dryRunHassEval` to preserve current behavior.
- Build clean (no warnings).
## Verification
After each commit:
- `cabal build` succeeds.
- After commit 2: `cabal build --ghc-options="-Wall -Wincomplete-uni-patterns -Wincomplete-record-updates"` produces zero warnings.
- `app/Main.hs` still compiles and imports `defaultMain` from its new home.
## Non-goals
- Splitting `HomeAssistant.Controller` into Effect/Entity/Controller sub-modules (deferred until real controllers exist).
- Adding tests (test suite is a placeholder; out of scope).
- Changing `app` to call real `hassEval` (stays on `dryRunHassEval` to preserve behavior).
- Any behavioral changes beyond warning fixes and the dormant `wsCallService` activation (which is itself inert until `app` switches to `hassEval`).