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

6.2 KiB
Raw Blame History

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.EffectHASSEff, 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:

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