warmcast 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. warmcast-0.1.0/.gitignore +7 -0
  2. warmcast-0.1.0/.import_linter_cache/.gitignore +2 -0
  3. warmcast-0.1.0/.import_linter_cache/1611e1dbfe0bcbc648563772f7e81bb2fa792146.data.json +1 -0
  4. warmcast-0.1.0/.import_linter_cache/CACHEDIR.TAG +3 -0
  5. warmcast-0.1.0/.import_linter_cache/warmcast.meta.json +1 -0
  6. warmcast-0.1.0/AGENTS.md +48 -0
  7. warmcast-0.1.0/CLAUDE.md +49 -0
  8. warmcast-0.1.0/PKG-INFO +11 -0
  9. warmcast-0.1.0/README.md +8 -0
  10. warmcast-0.1.0/addon/Dockerfile +7 -0
  11. warmcast-0.1.0/addon/README.md +61 -0
  12. warmcast-0.1.0/addon/config.yaml +30 -0
  13. warmcast-0.1.0/docs/decisions.md +28 -0
  14. warmcast-0.1.0/docs/design/core-engine.md +313 -0
  15. warmcast-0.1.0/docs/roadmap.md +15 -0
  16. warmcast-0.1.0/docs/spec.md +335 -0
  17. warmcast-0.1.0/examples/reference-install/house.toml +29 -0
  18. warmcast-0.1.0/examples/reference-install/signals.toml +54 -0
  19. warmcast-0.1.0/notes/README.md +1 -0
  20. warmcast-0.1.0/pyproject.toml +57 -0
  21. warmcast-0.1.0/repository.yaml +2 -0
  22. warmcast-0.1.0/src/warmcast/__init__.py +0 -0
  23. warmcast-0.1.0/src/warmcast/core/__init__.py +156 -0
  24. warmcast-0.1.0/src/warmcast/core/backtest.py +112 -0
  25. warmcast-0.1.0/src/warmcast/core/engine.py +102 -0
  26. warmcast-0.1.0/src/warmcast/core/house.py +348 -0
  27. warmcast-0.1.0/src/warmcast/core/learning.py +473 -0
  28. warmcast-0.1.0/src/warmcast/core/model.py +578 -0
  29. warmcast-0.1.0/src/warmcast/core/ports.py +26 -0
  30. warmcast-0.1.0/src/warmcast/core/scoring.py +158 -0
  31. warmcast-0.1.0/src/warmcast/core/settings.py +42 -0
  32. warmcast-0.1.0/src/warmcast/core/signals.py +607 -0
  33. warmcast-0.1.0/src/warmcast/core/sun.py +53 -0
  34. warmcast-0.1.0/src/warmcast/ha_connector/__init__.py +2 -0
  35. warmcast-0.1.0/src/warmcast/ha_connector/app.py +160 -0
  36. warmcast-0.1.0/src/warmcast/ha_connector/binding.py +133 -0
  37. warmcast-0.1.0/src/warmcast/ha_connector/client.py +113 -0
  38. warmcast-0.1.0/src/warmcast/ha_connector/nightly.py +73 -0
  39. warmcast-0.1.0/src/warmcast/ha_connector/reading_log.py +73 -0
  40. warmcast-0.1.0/src/warmcast/ha_connector/sampler.py +49 -0
  41. warmcast-0.1.0/src/warmcast/offline/__init__.py +0 -0
  42. warmcast-0.1.0/src/warmcast/offline/cli.py +433 -0
  43. warmcast-0.1.0/src/warmcast/offline/csv_sources.py +103 -0
  44. warmcast-0.1.0/src/warmcast/offline/signal_map.py +108 -0
  45. warmcast-0.1.0/src/warmcast/storage/__init__.py +6 -0
  46. warmcast-0.1.0/src/warmcast/storage/json_store.py +101 -0
  47. warmcast-0.1.0/src/warmcast/storage/refit_record.py +47 -0
  48. warmcast-0.1.0/src/warmcast/web/__init__.py +9 -0
  49. warmcast-0.1.0/src/warmcast/web/dashboard.py +186 -0
  50. warmcast-0.1.0/src/warmcast/web/log_view.py +89 -0
  51. warmcast-0.1.0/src/warmcast/web/server.py +84 -0
  52. warmcast-0.1.0/tests/synthetic.py +189 -0
  53. warmcast-0.1.0/tests/test_backtest.py +135 -0
  54. warmcast-0.1.0/tests/test_ha_connector.py +341 -0
  55. warmcast-0.1.0/tests/test_house.py +88 -0
  56. warmcast-0.1.0/tests/test_json_store.py +110 -0
  57. warmcast-0.1.0/tests/test_learning.py +265 -0
  58. warmcast-0.1.0/tests/test_model.py +255 -0
  59. warmcast-0.1.0/tests/test_offline.py +69 -0
  60. warmcast-0.1.0/tests/test_signals.py +252 -0
  61. warmcast-0.1.0/tests/test_thermostat.py +220 -0
  62. warmcast-0.1.0/tests/test_web.py +412 -0
  63. warmcast-0.1.0/web/app.css +245 -0
  64. warmcast-0.1.0/web/app.js +320 -0
  65. warmcast-0.1.0/web/chart.umd.min.js +1 -0
  66. warmcast-0.1.0/web/charts.html +57 -0
  67. warmcast-0.1.0/web/guide.html +92 -0
  68. warmcast-0.1.0/web/index.html +33 -0
  69. warmcast-0.1.0/web/plan.html +41 -0
  70. warmcast-0.1.0/web/settings.html +28 -0
@@ -0,0 +1,7 @@
1
+ # Private notes: never commit
2
+ private/
3
+ .DS_Store
4
+ __pycache__/
5
+ *.pyc
6
+ .venv/
7
+ CLAUDE.local.md
@@ -0,0 +1,2 @@
1
+ # Automatically created by Grimp.
2
+ *
@@ -0,0 +1 @@
1
+ {"imports_by_module":{"warmcast":[],"warmcast.core":[["warmcast.core.house",false,5,"from .house import ("],["warmcast.core.learning",false,22,"from .learning import ("],["warmcast.core.signals",false,59,"from .signals import ("],["warmcast.core.engine",false,4,"from .engine import Engine"],["warmcast.core.ports",false,56,"from .ports import HistorySource, ModelStore"],["warmcast.core.model",false,32,"from .model import ("],["warmcast.core.settings",false,58,"from .settings import LearnSettings"],["warmcast.core.scoring",false,57,"from .scoring import PvForecast, ZoneDayScore, nightly_cutoff, scorable_days, score_day"],["warmcast.core.backtest",false,3,"from .backtest import GATE2_DAYS, GATE2_MAE_C, NightlyFit, Scorecard, ZoneSummary, walk_forward"],["warmcast.core.sun",false,76,"from .sun import morning_weight, sun_position"]],"warmcast.core.backtest":[["datetime",false,8,"from datetime import date, timedelta"],["collections",false,6,"from collections.abc import Mapping"],["warmcast.core.settings",false,16,"from .settings import LearnSettings"],["time",false,5,"import time"],["warmcast.core.model",false,14,"from .model import ThermalModel, prior_model"],["warmcast.core.signals",false,17,"from .signals import History"],["warmcast.core.house",false,12,"from .house import House, ZoneId"],["numpy",false,10,"import numpy as np"],["dataclasses",false,7,"from dataclasses import dataclass"],["warmcast.core.learning",false,13,"from .learning import Accepted, FitOutcome, learn"],["warmcast.core.scoring",false,15,"from .scoring import ZoneDayScore, nightly_cutoff, scorable_days, score_day"],["__future__",false,3,"from __future__ import annotations"]],"warmcast.core.engine":[["warmcast.core.model",false,12,"from .model import Drivers, ThermalModel, Trajectory, ZoneState, estimate_state, prior_model"],["warmcast.core.backtest",false,9,"from .backtest import Scorecard, walk_forward"],["zoneinfo",false,7,"from zoneinfo import ZoneInfo"],["datetime",false,6,"from datetime import UTC, date, datetime, time, timedelta"],["warmcast.core.scoring",false,15,"from .scoring import nightly_cutoff"],["warmcast.core.signals",false,17,"from .signals import History, build_history, house_feeds"],["warmcast.core.ports",false,14,"from .ports import HistorySource, ModelStore"],["warmcast.core.settings",false,16,"from .settings import LearnSettings"],["warmcast.core.learning",false,11,"from .learning import Accepted, FitOutcome, KeptPrevious, estimate_dhw_baseline, learn"],["collections",false,5,"from collections.abc import Mapping"],["warmcast.core.house",false,10,"from .house import House, ZoneId"],["warmcast.core.model",false,13,"from .model import simulate as simulate_model"],["__future__",false,3,"from __future__ import annotations"]],"warmcast.core.house":[["dataclasses",false,4,"from dataclasses import dataclass"],["zoneinfo",false,8,"from zoneinfo import ZoneInfo, ZoneInfoNotFoundError"],["collections",false,3,"from collections.abc import Mapping"],["datetime",false,5,"from datetime import UTC, date, datetime, time"],["enum",false,6,"from enum import Enum"],["typing",false,7,"from typing import Any, NewType"],["__future__",false,1,"from __future__ import annotations"]],"warmcast.core.learning":[["enum",false,9,"from enum import Enum"],["scipy",false,12,"import scipy.optimize"],["warmcast.core.scoring",false,29,"from .scoring import nightly_cutoff, scorable_days, score_day"],["warmcast.core.signals",false,31,"from .signals import Array, HeatSource, History, Segment"],["dataclasses",false,7,"from dataclasses import dataclass, fields, replace"],["warmcast.core.settings",false,30,"from .settings import LearnSettings"],["numpy",false,11,"import numpy as np"],["__future__",false,3,"from __future__ import annotations"],["warmcast.core.model",false,15,"from .model import ("],["warmcast.core.house",false,14,"from .house import House, ZoneId"],["datetime",false,8,"from datetime import date, datetime, timedelta"],["math",false,5,"import math"],["collections",false,6,"from collections.abc import Mapping, Sequence"]],"warmcast.core.model":[["warmcast.core.signals",false,30,"from .signals import Array, Segment"],["warmcast.core.sun",false,31,"from .sun import morning_weight"],["__future__",false,7,"from __future__ import annotations"],["functools",false,14,"from functools import cached_property"],["numpy",false,16,"import numpy as np"],["warmcast.core.house",false,19,"from .house import ("],["json",false,10,"import json"],["scipy",false,17,"import scipy.linalg"],["collections",false,11,"from collections.abc import Mapping"],["hashlib",false,9,"import hashlib"],["dataclasses",false,12,"from dataclasses import asdict, dataclass, field, fields"],["datetime",false,13,"from datetime import datetime, timedelta"]],"warmcast.core.ports":[["__future__",false,1,"from __future__ import annotations"],["typing",false,5,"from typing import Protocol"],["warmcast.core.signals",false,7,"from .signals import Feed, RawSeries"],["datetime",false,4,"from datetime import datetime"],["collections",false,3,"from collections.abc import Mapping"]],"warmcast.core.scoring":[["warmcast.core.signals",false,15,"from .signals import Array, History, Segment"],["warmcast.core.settings",false,14,"from .settings import LearnSettings"],["dataclasses",false,6,"from dataclasses import dataclass, replace"],["collections",false,5,"from collections.abc import Iterator, Mapping"],["warmcast.core.model",false,13,"from .model import ThermalModel, drivers_from_segment, estimate_state, simulate"],["warmcast.core.house",false,12,"from .house import House, ZoneId"],["__future__",false,3,"from __future__ import annotations"],["numpy",false,10,"import numpy as np"],["zoneinfo",false,8,"from zoneinfo import ZoneInfo"],["datetime",false,7,"from datetime import UTC, date, datetime, time, timedelta"]],"warmcast.core.settings":[["__future__",false,1,"from __future__ import annotations"],["datetime",false,5,"from datetime import time"],["dataclasses",false,4,"from dataclasses import asdict, dataclass"],["json",false,3,"import json"]],"warmcast.core.signals":[["__future__",false,1,"from __future__ import annotations"],["enum",false,8,"from enum import Enum"],["itertools",false,3,"import itertools"],["warmcast.core.house",false,13,"from .house import House, ZoneId"],["numpy",false,10,"import numpy as np"],["math",false,4,"import math"],["datetime",false,7,"from datetime import UTC, datetime, timedelta"],["numpy",false,11,"from numpy.typing import NDArray"],["collections",false,5,"from collections.abc import Iterable, Iterator, Mapping"],["dataclasses",false,6,"from dataclasses import dataclass"]],"warmcast.core.sun":[["warmcast.core.signals",false,14,"from .signals import Array"],["numpy",false,11,"import numpy as np"],["warmcast.core.house",false,13,"from .house import House"],["__future__",false,7,"from __future__ import annotations"],["datetime",false,9,"from datetime import datetime, timedelta"]],"warmcast.ha_connector":[],"warmcast.ha_connector.app":[["collections",false,17,"from collections.abc import Callable, Mapping"],["warmcast.ha_connector.binding",false,27,"from .binding import Binding, BindingError, load_binding"],["tomllib",false,16,"import tomllib"],["pathlib",false,20,"from pathlib import Path"],["sys",false,13,"import sys"],["zoneinfo",false,22,"from zoneinfo import ZoneInfo"],["json",false,10,"import json"],["warmcast.ha_connector.reading_log",false,30,"from .reading_log import ReadingLog, ReadingLogError"],["logging",false,11,"import logging"],["warmcast.core",false,24,"from warmcast.core import House, HouseConfigError, LearnSettings, parse_house"],["warmcast.ha_connector.sampler",false,31,"from .sampler import Sampler"],["os",false,12,"import os"],["time",false,15,"import time"],["warmcast.web",false,25,"from warmcast.web import Dashboard, describe_columns, make_server, serve_in_background"],["warmcast.ha_connector.client",false,28,"from .client import HomeAssistantError, SupervisorClient"],["__future__",false,8,"from __future__ import annotations"],["typing",false,21,"from typing import Any"],["threading",false,14,"import threading"],["dataclasses",false,18,"from dataclasses import dataclass"],["datetime",false,19,"from datetime import UTC, date, datetime, timedelta"],["warmcast.ha_connector.nightly",false,29,"from .nightly import READING_LOG, refit_and_record"]],"warmcast.ha_connector.binding":[["re",false,27,"import re"],["datetime",false,31,"from datetime import timedelta"],["warmcast.offline.signal_map",false,36,"from warmcast.offline.signal_map import LoggerMap, SignalMapError, load_signal_map"],["pathlib",false,32,"from pathlib import Path"],["dataclasses",false,30,"from dataclasses import dataclass"],["__future__",false,25,"from __future__ import annotations"],["tomllib",false,28,"import tomllib"],["collections",false,29,"from collections.abc import Mapping"],["warmcast.core",false,35,"from warmcast.core import House, Signal, house_feeds"],["typing",false,33,"from typing import Any"]],"warmcast.ha_connector.client":[["os",false,10,"import os"],["datetime",false,16,"from datetime import datetime"],["typing",false,17,"from typing import Any, Protocol"],["json",false,9,"import json"],["collections",false,14,"from collections.abc import Mapping, Sequence"],["dataclasses",false,15,"from dataclasses import dataclass"],["urllib",false,11,"import urllib.error"],["__future__",false,7,"from __future__ import annotations"],["urllib",false,12,"import urllib.parse"],["urllib",false,13,"import urllib.request"]],"warmcast.ha_connector.nightly":[["__future__",false,7,"from __future__ import annotations"],["warmcast.ha_connector.binding",false,17,"from .binding import Binding"],["warmcast.storage",false,15,"from warmcast.storage import JsonModelStore, RefitRecord, RefitResult, save_refit"],["pathlib",false,11,"from pathlib import Path"],["warmcast.offline.csv_sources",false,14,"from warmcast.offline.csv_sources import LoggerCsv"],["datetime",false,10,"from datetime import datetime"],["logging",false,9,"import logging"],["warmcast.core",false,13,"from warmcast.core import Accepted, Engine, FitOutcome, House, KeptPrevious, LearnSettings"]],"warmcast.ha_connector.reading_log":[["io",false,10,"import io"],["collections",false,12,"from collections.abc import Sequence"],["csv",false,9,"import csv"],["__future__",false,7,"from __future__ import annotations"],["os",false,11,"import os"],["pathlib",false,13,"from pathlib import Path"]],"warmcast.ha_connector.sampler":[["datetime",false,4,"from datetime import UTC, datetime"],["__future__",false,1,"from __future__ import annotations"],["warmcast.ha_connector.reading_log",false,9,"from .reading_log import ReadingLog"],["warmcast.ha_connector.binding",false,7,"from .binding import Binding, EntityRef"],["typing",false,5,"from typing import Any"],["logging",false,3,"import logging"],["warmcast.ha_connector.client",false,8,"from .client import EntityState, HomeAssistantError, HomeAssistantReader"]],"warmcast.offline":[],"warmcast.offline.cli":[["__future__",false,1,"from __future__ import annotations"],["collections",false,6,"from collections.abc import Mapping"],["warmcast.core",false,12,"from warmcast.core import ("],["warmcast.offline.csv_sources",false,43,"from .csv_sources import LoggerCsv, StatisticsCsv"],["argparse",false,3,"import argparse"],["sys",false,4,"import sys"],["numpy",false,10,"import numpy as np"],["datetime",false,7,"from datetime import UTC, date, datetime, timedelta"],["pathlib",false,8,"from pathlib import Path"],["tomllib",false,5,"import tomllib"],["warmcast.storage",false,41,"from warmcast.storage import JsonModelStore"],["warmcast.offline.signal_map",false,44,"from .signal_map import SignalMap, SignalMapError, load_signal_map"]],"warmcast.offline.csv_sources":[["datetime",false,7,"from datetime import UTC, datetime"],["__future__",false,1,"from __future__ import annotations"],["itertools",false,4,"import itertools"],["warmcast.core",false,10,"from warmcast.core import Aggregation, Feed, HourlyStats, RawSeries, RawValue, Samples, Signal"],["dataclasses",false,6,"from dataclasses import dataclass"],["warmcast.offline.signal_map",false,12,"from .signal_map import LoggerMap, StatisticsMap"],["csv",false,3,"import csv"],["pathlib",false,8,"from pathlib import Path"],["collections",false,5,"from collections.abc import Mapping"]],"warmcast.offline.signal_map":[["typing",false,8,"from typing import Any"],["pathlib",false,7,"from pathlib import Path"],["datetime",false,6,"from datetime import timedelta"],["dataclasses",false,5,"from dataclasses import dataclass"],["warmcast.core",false,10,"from warmcast.core import Aggregation, Feed, Signal, ZoneId"],["tomllib",false,3,"import tomllib"],["collections",false,4,"from collections.abc import Mapping"],["__future__",false,1,"from __future__ import annotations"]],"warmcast.storage":[["warmcast.storage.refit_record",false,4,"from .refit_record import RefitRecord, RefitResult, load_refit, save_refit"],["warmcast.storage.json_store",false,3,"from .json_store import JsonModelStore"]],"warmcast.storage.json_store":[["warmcast.core",false,10,"from warmcast.core import ("],["datetime",false,6,"from datetime import datetime"],["dataclasses",false,5,"from dataclasses import asdict, fields"],["typing",false,8,"from typing import Any"],["json",false,3,"import json"],["__future__",false,1,"from __future__ import annotations"],["pathlib",false,7,"from pathlib import Path"],["os",false,4,"import os"]],"warmcast.storage.refit_record":[["enum",false,9,"from enum import Enum"],["__future__",false,3,"from __future__ import annotations"],["dataclasses",false,7,"from dataclasses import dataclass"],["json",false,5,"import json"],["os",false,6,"import os"],["pathlib",false,10,"from pathlib import Path"],["datetime",false,8,"from datetime import datetime"]],"warmcast.web":[["warmcast.web.server",false,7,"from .server import make_server, serve_in_background"],["warmcast.web.dashboard",false,6,"from .dashboard import Column, Dashboard, describe_columns"]],"warmcast.web.dashboard":[["warmcast.web.log_view",false,20,"from .log_view import Row, rows_since, summarise"],["warmcast.core",false,16,"from warmcast.core import House, LearnSettings, Signal"],["datetime",false,11,"from datetime import datetime, timedelta"],["warmcast.offline.signal_map",false,17,"from warmcast.offline.signal_map import LoggerMap"],["enum",false,12,"from enum import Enum"],["dataclasses",false,10,"from dataclasses import dataclass"],["warmcast.storage",false,18,"from warmcast.storage import load_refit"],["__future__",false,7,"from __future__ import annotations"],["typing",false,14,"from typing import Any"],["collections",false,9,"from collections.abc import Mapping"],["pathlib",false,13,"from pathlib import Path"]],"warmcast.web.log_view":[["datetime",false,11,"from datetime import datetime"],["pathlib",false,12,"from pathlib import Path"],["dataclasses",false,10,"from dataclasses import dataclass"],["__future__",false,7,"from __future__ import annotations"],["csv",false,9,"import csv"]],"warmcast.web.server":[["logging",false,11,"import logging"],["pathlib",false,17,"from pathlib import Path"],["__future__",false,8,"from __future__ import annotations"],["collections",false,14,"from collections.abc import Callable, Mapping"],["warmcast.web.dashboard",false,21,"from .dashboard import Dashboard, readings, settings, status"],["datetime",false,15,"from datetime import UTC, datetime"],["threading",false,13,"import threading"],["mimetypes",false,12,"import mimetypes"],["http",false,16,"from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer"],["typing",false,18,"from typing import Any"],["json",false,10,"import json"],["urllib",false,19,"from urllib.parse import urlsplit"]]},"version":2}
@@ -0,0 +1,3 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag automatically created by Grimp.
3
+ # For information about cache directory tags see https://bford.info/cachedir/
@@ -0,0 +1 @@
1
+ {"warmcast": 1794141363.474377, "warmcast.core.learning": 1794177935.2527387, "warmcast.ha_connector": 1794179163.667668, "warmcast.ha_connector.binding": 1794179163.675702, "warmcast.storage": 1794180419.8382998, "warmcast.core.house": 1794177935.2470284, "warmcast.web.log_view": 1794180419.853937, "warmcast.core.backtest": 1794157114.0756273, "warmcast.web": 1794180419.8452218, "warmcast.core.model": 1794177935.2583046, "warmcast.ha_connector.app": 1794181470.0350342, "warmcast.offline.csv_sources": 1794141363.4755564, "warmcast.core.sun": 1794177935.2667928, "warmcast.offline": 1794141363.4752448, "warmcast.ha_connector.reading_log": 1794179163.6881475, "warmcast.core.scoring": 1794177935.2623255, "warmcast.core.ports": 1794141363.474938, "warmcast.core.engine": 1794177935.242703, "warmcast.ha_connector.sampler": 1794179163.6915927, "warmcast.ha_connector.nightly": 1794180419.834461, "warmcast.offline.cli": 1794177935.2715244, "warmcast.storage.json_store": 1794177935.2759578, "warmcast.ha_connector.client": 1794181424.225544, "warmcast.web.dashboard": 1794181424.2261527, "warmcast.core.settings": 1794157114.093296, "warmcast.offline.signal_map": 1794141363.4756885, "warmcast.core": 1794177935.2376137, "warmcast.core.signals": 1794157114.097916, "warmcast.storage.refit_record": 1794180419.8417583, "warmcast.web.server": 1794181424.2267637}
@@ -0,0 +1,48 @@
1
+ # Warmcast: project guide for Codex
2
+
3
+ Warmcast is an open-source Home Assistant **app (add-on)** that learns how a home gains and loses heat and plans heat pump heating ahead in 30-minute slots. It stores heat in the building fabric (walls and floor slabs) and the hot water tank when energy is cheapest or most efficient, keeps every room in its comfort band, and reduces heating from the solar forecast so sunny days don't overheat the house.
4
+
5
+ The full spec is in [docs/spec.md](docs/spec.md). The roadmap is in [docs/roadmap.md](docs/roadmap.md). Decisions made so far, with the reasons, are in [docs/decisions.md](docs/decisions.md). Read those before proposing design changes.
6
+
7
+ Maintainer-only notes live in `CLAUDE.local.md` and `private/`, both gitignored. Read them when present.
8
+
9
+ ## Current status (8 Nov 2026)
10
+
11
+ - **Phase 1: collecting heating-on data** on the reference install. No Warmcast code exists yet.
12
+ - The working prototype is a set of Home Assistant helpers and automations on the reference install.
13
+ - **`docs/spec.md` is the source of truth** for the spec.
14
+
15
+ ## Hard rules
16
+
17
+ 1. **The core engine has no Home Assistant imports.** Everything platform-specific sits behind adapter interfaces (sensor reads, control writes, tariff, solar forecast, history, publishing/UI). This keeps the engine reusable.
18
+ 2. **Comfort first, then savings.** Every control write is bounded: zone caps inside the comfort band, flow offset inside the user's range, tank setpoints inside the tank limits.
19
+ 3. **Warmcast is never the only safety net.** The user's own thermostats and automations must keep the house warm if Warmcast stops.
20
+ 4. **Learn, don't assume.** A heat pump's flow temperature comes from its weather curve and is not an input; only boiler users enter a flow temperature. Heat loss, fabric capacity, solar gain and COP are learned from real data.
21
+ 5. **Predbat is optional and never required.** The fallbacks are Forecast.Solar, Solcast or Open-Meteo Solar for the solar forecast, and Octopus Energy or EDF Energy integrations (or a tariff select or fixed rates) for the tariff.
22
+ 6. **Start in shadow mode.** It plans but doesn't act until the user switches to Advisory or Control.
23
+ 7. **Public files carry no personal details.** No owner names, addresses or hostnames in docs, code, commits or PRs. Describe the reference install generically.
24
+ 8. **Public contributions carry no personal name or email and no Codex attribution.** Commits use the project git identity, and Co-Authored-By and "Generated with Codex" lines stay out of public commits and PRs.
25
+
26
+ ## Writing style
27
+
28
+ - UK English, plain words. Explain every setting and plan decision the way the app itself will.
29
+
30
+ ## Code
31
+
32
+ - **Language: Python**, matching Predbat and AppDaemon and common across Home Assistant projects.
33
+
34
+ | Part | Contents | Depends on HA? |
35
+ | --- | --- | --- |
36
+ | `core/` | Data model, thermal model, learning, planner, optimiser | No |
37
+ | `ha_connector/` | Feeds, device templates, controls, published sensors | Yes |
38
+ | `web/` | App dashboard: Overview, Plan, Charts, Settings, User guide (served through HA ingress) | Served by the app |
39
+ | `templates/` | Device templates (Daikin Altherma, Heatmiser Neo, Ring, Forecast.Solar, Solcast, Open-Meteo Solar, Octopus, EDF, Predbat), each with test fixtures | Data only |
40
+ | `addon/` | HA app packaging: Dockerfile, `config.yaml`, repository metadata | Yes |
41
+
42
+ Open choices are listed at the end of the spec (web UI framework, charting library, licence). Ask the maintainer before settling any of them.
43
+
44
+ ## Folder map
45
+
46
+ - `docs/`: public spec, roadmap and decision log
47
+ - `notes/`: working notes that are safe to make public
48
+ - `private/`: gitignored; internal notes, the reference install details and exported data
@@ -0,0 +1,49 @@
1
+ # Warmcast: project guide for Claude
2
+
3
+ Warmcast is an open-source Home Assistant **app (add-on)** that learns how a home gains and loses heat and plans heat pump heating ahead in 30-minute slots. It stores heat in the building fabric (walls and floor slabs) and the hot water tank when energy is cheapest or most efficient, keeps every room in its comfort band, and reduces heating from the solar forecast so sunny days don't overheat the house.
4
+
5
+ The full spec is in [docs/spec.md](docs/spec.md). The roadmap is in [docs/roadmap.md](docs/roadmap.md). Decisions made so far, with the reasons, are in [docs/decisions.md](docs/decisions.md). Read those before proposing design changes.
6
+
7
+ Maintainer-only notes live in `CLAUDE.local.md` and `private/`, both gitignored. Read them when present.
8
+
9
+ ## Current status (8 Nov 2026)
10
+
11
+ - **Phase 2: fitting the thermal model.** The core engine is built in `src/warmcast/core` (data loading, thermal model, learning, scoring, backtest), with the offline `warmcast-lab` command. Design and implementation notes: [docs/design/core-engine.md](docs/design/core-engine.md).
12
+ - Gate 1 still needs cold days; Gate 2 (peaks within about 0.5 °C for a week) is not met yet.
13
+ - **`docs/spec.md` is the source of truth** for the spec.
14
+ - Run checks with `ruff check`, `ruff format --check`, `lint-imports` and `pytest` (with `PYTHONPATH=src`).
15
+
16
+ ## Hard rules
17
+
18
+ 1. **The core engine has no Home Assistant imports.** Everything platform-specific sits behind adapter interfaces (sensor reads, control writes, tariff, solar forecast, history, publishing/UI). This keeps the engine reusable.
19
+ 2. **Comfort first, then savings.** Every control write is bounded: zone caps inside the comfort band, flow offset inside the user's range, tank setpoints inside the tank limits.
20
+ 3. **Warmcast is never the only safety net.** The user's own thermostats and automations must keep the house warm if Warmcast stops.
21
+ 4. **Learn, don't assume.** A heat pump's flow temperature comes from its weather curve and is not an input; only boiler users enter a flow temperature. Heat loss, fabric capacity, solar gain and COP are learned from real data.
22
+ 5. **Predbat is optional and never required.** The fallbacks are Forecast.Solar, Solcast or Open-Meteo Solar for the solar forecast, and Octopus Energy or EDF Energy integrations (or a tariff select or fixed rates) for the tariff.
23
+ 6. **Start in shadow mode.** It plans but doesn't act until the user switches to Advisory or Control.
24
+ 7. **Public files carry no personal details.** No owner names, addresses or hostnames in docs, code, commits or PRs. Describe the reference install generically.
25
+ 8. **Public contributions carry no personal name or email and no Claude attribution.** Commits use the project git identity, and Co-Authored-By and "Generated with Claude" lines stay out of public commits and PRs.
26
+
27
+ ## Writing style
28
+
29
+ - UK English, plain words. Explain every setting and plan decision the way the app itself will.
30
+
31
+ ## Code
32
+
33
+ - **Language: Python**, matching Predbat and AppDaemon and common across Home Assistant projects.
34
+
35
+ | Part | Contents | Depends on HA? |
36
+ | --- | --- | --- |
37
+ | `core/` | Data model, thermal model, learning, planner, optimiser | No |
38
+ | `ha_connector/` | Feeds, device templates, controls, published sensors | Yes |
39
+ | `web/` | App dashboard: Overview, Plan, Charts, Settings, User guide (served through HA ingress) | Served by the app |
40
+ | `templates/` | Device templates (Daikin Altherma, Heatmiser Neo, Ring, Forecast.Solar, Solcast, Open-Meteo Solar, Octopus, EDF, Predbat), each with test fixtures | Data only |
41
+ | `addon/` | HA app packaging: Dockerfile, `config.yaml`, repository metadata | Yes |
42
+
43
+ Open choices are listed at the end of the spec (web UI framework, charting library, licence). Ask the maintainer before settling any of them.
44
+
45
+ ## Folder map
46
+
47
+ - `docs/`: public spec, roadmap and decision log
48
+ - `notes/`: working notes that are safe to make public
49
+ - `private/`: gitignored; internal notes, the reference install details and exported data
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.5
2
+ Name: warmcast
3
+ Version: 0.1.0
4
+ Summary: Learns how a home gains and loses heat and plans heat pump heating ahead.
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: numpy>=2.0
7
+ Requires-Dist: scipy>=1.13
8
+ Provides-Extra: dev
9
+ Requires-Dist: import-linter>=2.0; extra == 'dev'
10
+ Requires-Dist: pytest>=8; extra == 'dev'
11
+ Requires-Dist: ruff>=0.6; extra == 'dev'
@@ -0,0 +1,8 @@
1
+ # Warmcast
2
+
3
+ *Work in progress, not ready to install.*
4
+
5
+ Warmcast is an open-source Home Assistant app that learns how your home gains and loses heat, and plans your heat pump's heating ahead. It stores heat in the building fabric (walls and floor slabs) and the hot water tank when energy is cheapest or most efficient, keeps every room comfortable, and uses the solar forecast so sunny days need less heating.
6
+
7
+ - [Spec](docs/spec.md)
8
+ - [Roadmap](docs/roadmap.md)
@@ -0,0 +1,7 @@
1
+ FROM python:3.12-slim
2
+
3
+ # The app is installed from the tagged release that matches config.yaml's version.
4
+ ARG WARMCAST_SOURCE=https://github.com/nighthawk944/warmcast/archive/refs/tags/v0.1.0.tar.gz
5
+ RUN pip install --no-cache-dir "${WARMCAST_SOURCE}"
6
+
7
+ CMD ["warmcast-app"]
@@ -0,0 +1,61 @@
1
+ # Warmcast app
2
+
3
+ Warmcast learns how your home gains and loses heat, so it can later plan your heat pump's heating ahead.
4
+
5
+ ## What this version does
6
+
7
+ - Every minute (you can change this), it reads the sensors you list and adds one row to its own reading log, `/data/readings.csv`. A sensor that is unavailable leaves its cell empty, never zero.
8
+ - Each night at 22:00 it tries to learn a model of your house from that log and writes the result to the app log and to the dashboard. With only a few days of readings it says it does not have enough data yet, and why.
9
+
10
+ This version only reads from Home Assistant. It publishes no sensors, changes no settings and makes no plans. Your thermostats and automations run your heating exactly as before.
11
+
12
+ ## The dashboard
13
+
14
+ Open Warmcast from the Home Assistant sidebar. It has five tabs:
15
+
16
+ - **Overview** shows whether the reading log is being written, how many readings it holds, the last reading, what the last nightly refit said, and whether each sensor had a reading at the last sample.
17
+ - **Plan** is empty for now. The 24-hour plan arrives after Gate 2. Warmcast stays in shadow mode until you change it: it works out what it would do but does not act on it.
18
+ - **Charts** draws the last 24 hours of room, outdoor and heat-output readings from the log.
19
+ - **Settings** shows your house settings, with a plain explanation under each one. To change a setting, edit `house.toml` and restart the app.
20
+ - **User guide** explains what Warmcast does, what shadow mode means, and what Gate 1 and Gate 2 are.
21
+
22
+ The dashboard only reads the app's own files. It shows no forecasts yet, because there is no tested model to make them. The charts use a copy of Chart.js that comes with the app, so they need no internet access.
23
+
24
+ ## Setting it up
25
+
26
+ Put two files in this app's config folder (`/addon_configs/<id>_warmcast/` on your Home Assistant host):
27
+
28
+ - `house.toml` lists your rooms, their heating type and comfort range. See `examples/` in the repository.
29
+ - `signals.toml` says which entity feeds each reading:
30
+
31
+ ```toml
32
+ [logger]
33
+ time_column = "time"
34
+ hold_minutes = 2
35
+
36
+ [logger.signals]
37
+ outdoor_c = "outdoor_c"
38
+ heat_kw = "heat_kw"
39
+
40
+ [logger.zones.living_room]
41
+ air_c = "living_air_c"
42
+
43
+ [logger.entities]
44
+ outdoor_c = "sensor.outdoor_temperature"
45
+ heat_kw = "sensor.heat_pump_heat_output"
46
+ living_air_c = { entity = "climate.living_room", attribute = "current_temperature" }
47
+ ```
48
+
49
+ `[logger.signals]` and `[logger.zones.<zone>]` name a log column for each reading Warmcast understands. `[logger.entities]` names the entity behind each column, in the order the columns are written. `hold_minutes` must be longer than the sample interval. The same file lets `warmcast-lab` read the log on a computer.
50
+
51
+ If a reading your house settings need has no entity, the app stops at start-up and says which one is missing.
52
+
53
+ ## Options
54
+
55
+ | Option | Default | What it does |
56
+ | --- | --- | --- |
57
+ | `house_file` | `/config/house.toml` | Your house settings |
58
+ | `binding_file` | `/config/signals.toml` | Which entity feeds each reading |
59
+ | `sample_interval_seconds` | `60` | How often the sensors are read |
60
+ | `data_path` | `/data` | Where the reading log and the learned models are kept |
61
+ | `timezone` | `Europe/London` | Your time zone, for the nightly run |
@@ -0,0 +1,30 @@
1
+ name: Warmcast
2
+ version: "0.1.0"
3
+ slug: warmcast
4
+ description: >-
5
+ Learns how your home gains and loses heat. This version only reads your sensors and
6
+ keeps its own log; it changes nothing in Home Assistant.
7
+ arch:
8
+ - aarch64
9
+ - amd64
10
+ startup: application
11
+ boot: auto
12
+ # Lets the app read entity states through the Supervisor with its own token.
13
+ homeassistant_api: true
14
+ # The read-only dashboard, served by the app on this port. The port number is provisional.
15
+ ingress: true
16
+ ingress_port: 8099
17
+ panel_icon: mdi:home-thermometer
18
+ # The house settings and entity binding live in this app's own config folder.
19
+ map:
20
+ - addon_config
21
+ options:
22
+ house_file: /config/house.toml
23
+ binding_file: /config/signals.toml
24
+ sample_interval_seconds: 60
25
+ data_path: /data
26
+ schema:
27
+ house_file: str
28
+ binding_file: str
29
+ sample_interval_seconds: int(10,3600)
30
+ data_path: str
@@ -0,0 +1,28 @@
1
+ # Decision log
2
+
3
+ The decisions made while writing the spec (Oct–Nov 2026), newest first. Each one shows what was decided and why.
4
+
5
+ | Date | Decision | Why |
6
+ | --- | --- | --- |
7
+ | 8 Nov 2026 | **Dashboard: plain web pages with Chart.js.** No build step, light enough for a Raspberry Pi. | Covers the five tabs and charts without the upkeep of a full framework. |
8
+ | 8 Nov 2026 | **Platforms: 64-bit ARM and x86 supported; 32-bit ARM not.** scipy and numpy ship no 32-bit ARM builds. | Home Assistant no longer supports 32-bit ARM, so the app doesn't either. Supporting it would mean removing scipy from the fitting code. |
9
+ | 8 Nov 2026 | **Language: Python.** | Matches Predbat and AppDaemon, and is common across open-source Home Assistant projects. |
10
+ | 8 Nov 2026 | **Name: Warmcast**, replacing the working name HeatBank. Domain .app or .ai to be chosen; .com may follow later. | Short, says what it does (warmth from forecasts), and the .com is not held by a live product. |
11
+ | 8 Nov 2026 | **Building fabric means walls and floor slabs.** The thermal model treats both as stored heat, not just the underfloor slab. | Walls hold a lot of heat in every home, including those with radiators. |
12
+ | 8 Nov 2026 | **The reference install is described generically** in public docs: no owner name and no "my house". | Other people will use the docs as a guide to their own install. |
13
+ | 1 Nov 2026 | **Cold-weather preheat:** when the forecast shows a colder than normal spell or a threshold crossing (e.g. below 5°C, below 0°C, frost), raise the flow offset overnight to store heat in the fabric. Cut the preheat back by the expected solar gain. | Cheap overnight heat beats peak-rate heat and running the heat pump hardest in the coldest hours. Sunny cold days must not end up overheated. |
14
+ | 26 Oct 2026 | **The flow offset is a planned lever.** Cost mode raises it in free, low-rate and solar-divert slots. COP mode sets it back slightly overnight while people sleep. The user sets the baseline, range, sleep hours and night comfort band. | Weather-compensated heat pumps usually expose an offset, and it's the main way to move heat in time. |
15
+ | 25 Oct 2026 | **Octopus and EDF free electricity sessions are in the launch release**, not a later phase. | Free sessions are the biggest single win for storing heat. |
16
+ | 25 Oct 2026 | **EDF tariff source:** the unofficial [EDF Energy integration](https://github.com/stevekirtley/HomeAssistant-EDFEnergy) (HACS). It has rates but no free sessions, so EDF sessions need Warmcast's own connector or an upstream contribution. | It's the only EDF integration with rates; its README says free sessions were removed. |
17
+ | 25 Oct 2026 | **Predbat is optional.** Fall back to Forecast.Solar, Solcast or Open-Meteo Solar for the solar forecast, Octopus/EDF or a tariff select for rates, and the inverter integration for PV and battery. | Most users won't have Predbat. |
18
+ | 25 Oct 2026 | **Package as a Home Assistant app (add-on), like Predbat, not as a HACS integration.** At launch users add the repo URL to the app store; aim for a community listing later. | Logic runs locally on the user's hardware, with its own web UI. This replaced an earlier HACS plan. |
19
+ | 25 Oct 2026 | **App dashboard with five tabs:** Overview, Plan, Charts, Settings, User guide. Charts includes a live schematic (like ESPAltherma's, but much nicer), plus temperatures, energy, COP and the heat loss curve. | One place to see whether it's working, what it plans and why. |
20
+ | 25 Oct 2026 | **Don't assume a flow temperature.** Heat pumps follow their weather curve; boilers enter theirs. Learn the heat needed to reach target and the heat loss per hour at each outdoor temperature. | Assumptions break across installs; real data doesn't. |
21
+ | 25 Oct 2026 | **Positioning:** Warmcast adjusts the heating it delivers using the solar forecast, rather than only preventing overheating. No comparison with Predbat in the copy. | Clearer, stands on its own. |
22
+ | 24–25 Oct 2026 | **Door/window pause is set per zone:** Off, or pause after N minutes open (default 15). Shorter openings never pause. | Brief openings shouldn't stop heating. |
23
+ | 24 Oct 2026 | **Two optimisation modes:** COP (least electricity) and Cost (least money), with the same comfort rules. Optional minimum COP in cost mode. | Different tariffs suit different goals. |
24
+ | 24 Oct 2026 | **Hot water vs heating priority is the user's choice:** Hot water first, Heating first, or Balanced (default). Minimum tank and room temperatures always apply. Solar diversion goes to the tank first, and to the fabric only when zones are cooling and no big solar gain is due. | The heat pump can only heat one at a time. |
25
+ | 24 Oct 2026 | **Plan table:** 30-minute slots, a 24-hour plan view and a 48-hour actuals view (toggle, never both at once). Every slot has a one-line reason. | Like Predbat's plan, built for heat. |
26
+ | 24 Oct 2026 | **Rollout modes:** Shadow, then Advisory, then Control. Every install starts in Shadow. | Trust has to be earned on each house. |
27
+ | 24 Oct 2026 | **Generic by design:** any `climate` thermostat, any number of zones, optional floor sensors, emitter type per zone (screed UFH, overlay UFH, radiators), optional door/window sensors, optional hot water tank. | Not tied to the reference install's kit. |
28
+ | 24 Oct 2026 | **Working name: HeatBank** (heatbank.app free). Alternatives: Thermabank (thermabank.com free), Flowcast. Not final. | Avoid names too close to Predbat. |