daemon

package module
v0.0.0-...-82a72bb Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MPL-2.0 Imports: 41 Imported by: 0

Documentation

Index

Constants

View Source
const SolarAvailableTopic = "myhome/energy/solar/available"

SolarAvailableTopic is the retained MQTT topic where the daemon publishes the sum of all known solar-energy sources. Shelly device scripts (e.g. pool-pump.js) subscribe to it directly and decide for themselves whether to act on it — the daemon itself never issues a start/stop RPC based on this value, keeping devices daemon-optional (see AGENTS.md "Resilience Rules").

Variables

View Source
var Cmd = &cobra.Command{
	Use:   "daemon",
	Short: "MyHome Daemon",
	Long:  "MyHome Daemon, with embedded MQTT broker and persistent device manager",
	Args:  cobra.NoArgs,
}
View Source
var DefaultConfig = Config{
	RefreshInterval: 3 * time.Minute,
}

Functions

func NewDaemon

func NewDaemon(ctx context.Context) *daemon

Types

type Config

type Config struct {
	RefreshInterval time.Duration `json:"refresh_interval"`
}

type PoolNotices

type PoolNotices struct {
	// contains filtered or unexported fields
}

PoolNotices records a companion "pool.turnover_today" notice whenever the pool pump stops — either via the device's own pool.pump_stop (schedule or manual) or the daemon's pool.solar_stop — reporting the water-volume turnovers achieved today against the configured daily target. Since #402, pool-pump.js itself computes and persists today's cumulative runtime and achieved turnover to KVS (it owns the string->RPM speed mapping); this type just reads those pre-computed values back rather than re-deriving flow rate here.

func NewPoolNotices

func NewPoolNotices(ctx context.Context, log logr.Logger, eventsSvc *events.Service, deviceID string) *PoolNotices

NewPoolNotices builds a PoolNotices, or returns nil if any dependency is unavailable (events service disabled, or the pool device can't be reached over MQTT right now). OnEvent on a nil *PoolNotices is a safe no-op, so daemon.go can wire it into the broadcast hook unconditionally.

func (*PoolNotices) ActiveSpeed

func (p *PoolNotices) ActiveSpeed(ctx context.Context) (active bool, speedName string, err error)

ActiveSpeed reports whether the pool pump is currently running and, if so, which named speed (eco/mid/high) its active switch output maps to. It is a best-effort live read used to enrich the "pool-pump" entry in the solar.claimerslist RPC (see solar_rpc.go) — callers should apply their own timeout via ctx and treat a returned error as "unknown" rather than a reason to fail the whole request. ActiveSpeed on a nil *PoolNotices is a safe no-op (mirrors OnEvent), reporting inactive with no error.

func (*PoolNotices) ComputeTurnover

func (p *PoolNotices) ComputeTurnover(ctx context.Context) (achieved, target float64, runtimeSec int64, err error)

ComputeTurnover returns today's achieved water-volume turnovers (pool volumes filtered so far today) against the configured daily target, plus the runtime in seconds they were derived from. Shared by the pool.turnover_today notice (recordTurnoverToday) and the pool.getstatus RPC handler (PoolRPCHandler) so both read the same KVS values the same way. As of #402, both achieved turnover and runtime are computed on-device by pool-pump.js (which owns the preferred-speed -> RPM mapping) and mirrored to KVS — this just reads them back.

func (*PoolNotices) OnEvent

func (p *PoolNotices) OnEvent(ctx context.Context, e events.Event)

OnEvent is wired into the daemon's event broadcast hook (see daemon.go broadcastFn) alongside notice.Service.OnEvent. It reacts only to pool.pump_stop (device-emitted, schedule/manual) and pool.solar_stop (daemon-emitted) — every other event is a no-op.

func (*PoolNotices) WaterSupplyActive

func (p *PoolNotices) WaterSupplyActive(ctx context.Context) (bool, error)

WaterSupplyActive reports whether the pool device's water-supply protection input is currently engaged (true = active, pump forced off by pool-pump.js's handleWaterSupply; false = normal operation).

type PoolRPCHandler

type PoolRPCHandler struct {
	// contains filtered or unexported fields
}

PoolRPCHandler exposes the configured pool device's turnover rate and water-supply status via the myhome.PoolGetStatus RPC verb, so both the web UI and `ctl pool status` read from the same source. It reuses PoolNotices' already-initialized device handle and KVS helpers rather than standing up a second connection to the same device.

func NewPoolRPCHandler

func NewPoolRPCHandler(log logr.Logger, pool *PoolNotices) *PoolRPCHandler

NewPoolRPCHandler builds a PoolRPCHandler. pool may be nil (pool tracking disabled or the device unreachable at startup) — handleGetStatus then returns a clear error instead of panicking.

func (*PoolRPCHandler) RegisterHandlers

func (h *PoolRPCHandler) RegisterHandlers()

RegisterHandlers registers the pool.getstatus RPC method.

type PoolRuntimeTracker

type PoolRuntimeTracker struct {
	// contains filtered or unexported fields
}

PoolRuntimeTracker reports how long the pool pump has run today and how much runtime remains toward the daily filtration target. It reads directly from the shared events database populated by the gen2 listener — no separate pool.db, no MQTT subscription.

func NewPoolRuntimeTracker

func NewPoolRuntimeTracker(log logr.Logger, storage *events.Storage, deviceID string) *PoolRuntimeTracker

func (*PoolRuntimeTracker) DailyRuntimeSec

func (t *PoolRuntimeTracker) DailyRuntimeSec(ctx context.Context) (int64, error)

DailyRuntimeSec returns the total seconds the pump has run today, including any currently-running interval.

func (*PoolRuntimeTracker) RemainingRuntimeSec

func (t *PoolRuntimeTracker) RemainingRuntimeSec(ctx context.Context, targetSec int64) (int64, error)

RemainingRuntimeSec returns max(0, targetSec - DailyRuntimeSec()).

type PumpController

type PumpController interface {
	SetPump(ctx context.Context, on bool) error
}

PumpController abstracts switch control so the state machine can be tested without MQTT.

type RuntimeTracker

type RuntimeTracker interface {
	DailyRuntimeSec(ctx context.Context) (int64, error)
}

RuntimeTracker reports how long the pool pump has run today.

type SolarAggregator

type SolarAggregator struct {
	// contains filtered or unexported fields
}

SolarAggregator sums the last-known reading from every registered SolarSource and republishes the total to SolarAvailableTopic on every incoming reading (i.e. on whatever cadence sources report — currently Beem's ~60s poll interval). A source whose last reading is older than staleAfter is excluded from the sum but doesn't block other sources from reporting or being summed.

func NewSolarAggregator

func NewSolarAggregator(log logr.Logger, mc mqttclient.Client, staleAfter time.Duration, sources ...SolarSource) *SolarAggregator

NewSolarAggregator builds an aggregator over the given sources. Call Start to begin consuming readings and publishing the aggregate.

func (*SolarAggregator) Start

func (a *SolarAggregator) Start(ctx context.Context)

Start launches one forwarder goroutine per registered source. Every goroutine exits when ctx is done (or its source's channel closes on its own), so Start never leaks goroutines past ctx's lifetime.

type SolarAutomation

type SolarAutomation struct {
	// contains filtered or unexported fields
}

SolarAutomation subscribes to Beem power samples and controls the pool pump using a hysteresis state machine:

IDLE  →  (solar_w ≥ StartThresholdW  for  StartDelay
          AND  runtime < MaxRotationSec)                         →  RUNNING
RUNNING  →  (runtime ≥ MaxRotationSec)                           →  IDLE  [hard ceiling]
RUNNING  →  (runtime ≥ DailyTargetSec  AND  solar_w < StartThresholdW)
                                                                  →  IDLE  [soft stop]
RUNNING  →  (solar_w < StopThresholdW  for  StopDelay)           →  IDLE  [solar loss]

See docs/beem-energy.md "Soft stop vs. hard ceiling" for the rationale: DailyTargetSec only stops the pump once solar has also dropped, so free solar energy keeps over-filtering rather than going to waste; MaxRotationSec is an absolute ceiling that always stops (and blocks new solar starts).

func NewSolarAutomation

func NewSolarAutomation(
	log logr.Logger,
	powerCh <-chan beem.PowerSample,
	tracker RuntimeTracker,
	pump PumpController,
	cfg SolarConfig,
) *SolarAutomation

NewSolarAutomation creates a SolarAutomation but does not start it.

func (*SolarAutomation) Start

func (sa *SolarAutomation) Start(ctx context.Context)

Start launches the state-machine goroutine. It returns immediately. The goroutine stops when ctx is cancelled.

func (*SolarAutomation) WithEvents

func (sa *SolarAutomation) WithEvents(eventsSvc *events.Service, deviceID string) *SolarAutomation

WithEvents enables recording "notice"-severity pool.solar_start / pool.solar_stop events to eventsSvc, attributed to deviceID. Without this, the solar pump still operates identically — only the notice trail is skipped (degraded mode: daemon-down or events-disabled never blocks pump control, see CLAUDE.md "daemon-optional per device").

type SolarAvailablePayload

type SolarAvailablePayload struct {
	AvailableW float64            `json:"available_w"`
	TS         int64              `json:"ts"`
	Sources    []SolarSourceDebug `json:"sources,omitempty"`
}

SolarAvailablePayload is published (retained, QoS AtLeastOnce) to SolarAvailableTopic every time any registered source reports a reading.

TS uses unix-epoch-seconds (not RFC3339): pool-pump.js parses this on-device with mJS, where epoch arithmetic is cheaper/less error-prone than ISO-8601 parsing.

Staleness contract: the aggregator only recomputes and republishes when a reading arrives (see SolarAggregator doc comment). If every source stops reporting — e.g. Beem's REST API becomes unreachable because the internet is down — nothing republishes, so the retained topic keeps serving its last value indefinitely, with TS growing arbitrarily old. Because MQTT delivers a retained message to a new subscriber immediately regardless of its age, TS is not decorative: a subscriber (e.g. pool-pump.js in #405) MUST compare TS against its own staleness threshold before trusting AvailableW, rather than assuming a received message is fresh.

type SolarConfig

type SolarConfig struct {
	StartThresholdW float64       // start pump when solar_w >= this
	StopThresholdW  float64       // stop pump when solar_w < this
	StartDelay      time.Duration // solar must hold above start threshold for this long
	StopDelay       time.Duration // solar must hold below stop threshold for this long
	DailyTargetSec  int64         // soft-stop target in seconds; 0 = no soft-stop check
	MaxRotationSec  int64         // hard-ceiling in seconds; 0 = no ceiling
}

SolarConfig holds the hysteresis parameters for solar-driven pump control.

type SolarRPCHandler

type SolarRPCHandler struct {
	// contains filtered or unexported fields
}

SolarRPCHandler exposes the daemon's static energy-claimers registry via the myhome.SolarClaimersList RPC verb (mirrors PoolRPCHandler). It is deliberately not a live-arbitration engine: no priority ordering, no partial allocation — see the follow-up "solar router" issue (#401) for that. It best-effort-enriches the "pool-pump" claimer with a live active/speed read via PoolNotices.ActiveSpeed.

func NewSolarRPCHandler

func NewSolarRPCHandler(log logr.Logger, registry *energy.Registry, pool *PoolNotices) *SolarRPCHandler

NewSolarRPCHandler builds a SolarRPCHandler. pool may be nil (pool tracking disabled or the device unreachable at startup) — handleClaimersList then reports the pool-pump claimer's identity without live status, mirroring how PoolNotices.OnEvent on a nil receiver is already a no-op.

func (*SolarRPCHandler) RegisterHandlers

func (h *SolarRPCHandler) RegisterHandlers()

RegisterHandlers registers the solar.claimerslist RPC method.

type SolarReading

type SolarReading struct {
	Source string
	Watts  float64
	TS     time.Time
}

SolarReading is a single instantaneous power reading from a solar-energy source (e.g. a Beem PnP kit, or some other inverter brand in the future).

type SolarSource

type SolarSource interface {
	// Name identifies the source (e.g. "beem"). Used as the key in
	// SolarAggregator's last-reading map and in SolarSourceDebug.Name.
	Name() string

	// Subscribe returns a channel of readings tied to ctx's lifetime: the
	// returned channel is closed once ctx is done (or the underlying source
	// stops on its own).
	Subscribe(ctx context.Context) <-chan SolarReading
}

SolarSource is a generic solar-energy source that SolarAggregator can sum over. Implementations adapt a specific vendor watcher/API (e.g. pkg/beem.Watcher, via beemSolarSource) to this interface so the aggregator never needs to know how many sources exist or where they come from.

type SolarSourceDebug

type SolarSourceDebug struct {
	Name  string  `json:"name"`
	Watts float64 `json:"watts"`
	Stale bool    `json:"stale"`
}

SolarSourceDebug reports one source's contribution to AvailableW, for observability (e.g. debugging via `mosquitto_sub` or the web UI). Not consumed by pool-pump.js today — informational only.

Directories

Path Synopsis
watch module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL