Files
Cyrene_v2/internal/iot/manager.go
T

165 lines
5.2 KiB
Go

package iot
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
)
// Manager is the IoT subsystem facade. It selects a Provider from
// configuration, serves List/Control/Status, and publishes device state
// transitions on the in-process bus as DeviceUpdate events.
type Manager struct {
providers []Provider
bus bus.Bus
log *slog.Logger
}
// New builds a Manager from cfg, selecting the device Provider by cfg.Provider.
//
// Supported providers: "" or "mock" registers the built-in mock provider with
// eight simulated devices; "none" disables the subsystem (no provider is
// registered, List returns an empty slice and Control/Status fail with
// ErrDeviceNotFound). Any other value returns ErrUnknownProvider.
//
// log may be nil; it is replaced with a discard logger. The Manager owns a
// fresh, isolated bus exposed via Bus for subscribers to attach to.
func New(cfg config.IoTConfig, log *slog.Logger) (*Manager, error) {
m := &Manager{
bus: bus.New(),
log: logger.WithSource(discardLogger(log), "iot/manager"),
}
switch strings.TrimSpace(cfg.Provider) {
case "", "mock":
mp, err := newMockProvider(log)
if err != nil {
return nil, err
}
m.providers = append(m.providers, mp)
case "none":
// No provider is registered: the subsystem is disabled.
default:
return nil, fmt.Errorf("iot: new: provider %q: %w", cfg.Provider, ErrUnknownProvider)
}
m.log.Info("iot manager initialized", "provider", cfg.Provider, "providers", len(m.providers))
return m, nil
}
// Register adds p as a device source. Registering a provider whose Name already
// exists returns ErrDuplicate; a nil provider returns ErrInvalidArgument.
func (m *Manager) Register(p Provider) error {
if p == nil {
return fmt.Errorf("iot: register: %w", ErrInvalidArgument)
}
for _, existing := range m.providers {
if existing.Name() == p.Name() {
return fmt.Errorf("iot: register %q: %w", p.Name(), ErrDuplicate)
}
}
m.providers = append(m.providers, p)
m.log.Info("iot provider registered", "provider", p.Name())
return nil
}
// Bus returns the Manager's in-process event bus. Subscribers attach to
// DeviceUpdateSubject (or "iot.>") to observe device state transitions.
func (m *Manager) Bus() bus.Bus {
return m.bus
}
// List returns every device from every registered provider.
func (m *Manager) List(ctx context.Context) ([]Device, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
var out []Device
for _, p := range m.providers {
devices, err := p.List(ctx)
if err != nil {
return nil, fmt.Errorf("iot: list %q: %w", p.Name(), err)
}
out = append(out, devices...)
}
m.log.Debug("iot devices listed", "count", len(out))
return out, nil
}
// Control applies action to the owning provider's device and, on success,
// publishes a DeviceUpdate on the bus. It returns ErrDeviceNotFound when no
// provider owns the device.
func (m *Manager) Control(ctx context.Context, deviceID, action string, params map[string]any) (*Device, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if len(m.providers) == 0 {
return nil, fmt.Errorf("iot: control: %w", ErrProviderNotRegistered)
}
for _, p := range m.providers {
d, err := p.Control(ctx, deviceID, action, params)
if err != nil {
if errors.Is(err, ErrDeviceNotFound) {
continue
}
return nil, err
}
m.publishUpdate(ctx, p.Name(), *d, action, params)
return d, nil
}
return nil, fmt.Errorf("iot: control %q: %w", deviceID, ErrDeviceNotFound)
}
// Status returns the current state of the device owned by the matching provider.
// It returns ErrDeviceNotFound when no provider owns the device.
func (m *Manager) Status(ctx context.Context, deviceID string) (*Device, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if len(m.providers) == 0 {
return nil, fmt.Errorf("iot: status: %w", ErrProviderNotRegistered)
}
for _, p := range m.providers {
d, err := p.Status(ctx, deviceID)
if err != nil {
if errors.Is(err, ErrDeviceNotFound) {
continue
}
return nil, err
}
return d, nil
}
return nil, fmt.Errorf("iot: status %q: %w", deviceID, ErrDeviceNotFound)
}
// publishUpdate fans a DeviceUpdate out on the bus. A publish failure (for
// example a canceled context) is logged, not returned: the device state already
// changed, and control should not be reported as failed because an observer did
// not receive the event.
func (m *Manager) publishUpdate(ctx context.Context, provider string, d Device, action string, params map[string]any) {
if err := m.bus.Publish(ctx, DeviceUpdateSubject, DeviceUpdate{
Provider: provider,
Device: d,
Action: action,
Params: params,
}); err != nil {
m.log.Error("iot publish device update", "device", d.ID, "action", action, "error", err)
return
}
m.log.Debug("iot device update published", "device", d.ID, "action", action, "provider", provider)
}
// discardLogger returns log if non-nil, otherwise a logger that writes nowhere,
// so New always has a safe logger to use.
func discardLogger(log *slog.Logger) *slog.Logger {
if log != nil {
return log
}
return slog.New(slog.NewTextHandler(io.Discard, nil))
}