feat(iot): IoT 插件化(Provider/Manager/mock 8设备/能力约束/bus 事件);feat(capability/restrict): 工具级布尔标记(群聊/陌生人可用,默认关,策略引擎雏形)+ registry ToolPolicy
This commit is contained in:
1 parent
915aa8a100
commit
b3c61aa1ff
9 files changed
+1358
No files matched your search
@@ -0,0 +1,164 @@
|
||||
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))
|
||||
}
|
||||
Reference in new issue
Block a user