165 lines
5.2 KiB
Go
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))
|
|
}
|