// Package econet provides EconetService, a service.AppService that connects // to the Rheem EcoNet cloud and exposes device state via gRPC, an MCP tool, // and OTEL metrics served for Prometheus scraping. package econet import ( "context" "crypto/tls" "fmt" "log/slog" "net/http" rheemcloud "github.com/kevinburke/rheemcloud-go" optsgrpc "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts" optshttp "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" "gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/config" "gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/econet/econetgrpc" "gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/econet/econetmcp" "gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/econet/econetmetrics" "gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/service" ) type EconetService struct { ctx context.Context config *config.ServiceConfig client *rheemcloud.Client grpc *econetgrpc.EconetGRPCServer mcp *econetmcp.EconetMCPServer metrics *econetmetrics.Collector } func (e *EconetService) Init(ctx context.Context, cfg *config.ServiceConfig) (service.ShutdownFunc, error) { e.ctx = ctx e.config = cfg client, err := connect(ctx, cfg) if err != nil { return nil, err } e.client = client e.grpc = econetgrpc.NewEconetGRPCServer(ctx, cfg, client) e.mcp = econetmcp.NewEconetMCPServer(ctx, cfg, e.grpc) e.metrics = econetmetrics.NewCollector(ctx, cfg, client) if err := e.metrics.Start(); err != nil { return nil, err } return e.shutdown, nil } func connect(ctx context.Context, cfg *config.ServiceConfig) (*rheemcloud.Client, error) { if cfg.EconetEmail == "" || cfg.EconetPassword == "" { return nil, fmt.Errorf("econet: email and password required (set ECONET_EMAIL / ECONET_PASSWORD)") } client, err := rheemcloud.Connect(ctx, cfg.EconetEmail, cfg.EconetPassword, &rheemcloud.Config{ Logger: slog.Default(), HTTPClient: &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: cfg.EconetTLSInsecure, }, }, }, }) if err != nil { return nil, fmt.Errorf("econet: connect failed: %w", err) } return client, nil } func (e *EconetService) shutdown(_ context.Context) (string, error) { e.metrics.Stop() return "EconetService", e.client.Close() } func (e *EconetService) GetGRPC() *optsgrpc.AppGRPC { return &optsgrpc.AppGRPC{ Services: e.grpc.GetServices(), GRPCDialOpts: e.grpc.GetDialOpts(), } } func (e *EconetService) GetHTTP() *optshttp.AppHTTP { return &optshttp.AppHTTP{ Ctx: e.ctx, Handlers: e.mcp.GetHandlers(), HealthChecks: e.healthChecks(), } } func (e *EconetService) healthChecks() []optshttp.HealthCheckFunc { return []optshttp.HealthCheckFunc{ func(_ context.Context) error { if len(e.client.Devices()) == 0 { return fmt.Errorf("econet: no devices loaded") } return nil }, } }