- internal/plugin: Plugin/IoTProvider/ToolProvider/RAGProvider interfaces - internal/plugin: Registry with type-safe auto-detection, 11 tests - plugins/iot-mock: 8 simulated smart home devices, 11 tests - 63 total tests, build + vet clean
256 lines
6.1 KiB
Go
256 lines
6.1 KiB
Go
// Package iotmock implements a simulated IoT device provider for development and testing.
|
|
// It provides 8 virtual smart home devices with realistic behavior including
|
|
// sensor fluctuation and operation history tracking.
|
|
package iotmock
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/plugin"
|
|
)
|
|
|
|
// Plugin implements the IoTProvider interface with 8 simulated devices.
|
|
type Plugin struct {
|
|
devices map[string]*plugin.Device
|
|
subs []chan plugin.DeviceEvent
|
|
mu sync.RWMutex
|
|
stopCh chan struct{}
|
|
stopped bool
|
|
}
|
|
|
|
// New creates a new iot-mock plugin with 8 pre-configured devices.
|
|
func New() *Plugin {
|
|
return &Plugin{
|
|
devices: map[string]*plugin.Device{
|
|
"light-livingroom": {
|
|
ID: "light-livingroom", Name: "客厅灯", Type: plugin.DeviceLight,
|
|
State: map[string]any{"power": false, "brightness": 80, "color": "warm_white"},
|
|
Online: true,
|
|
},
|
|
"light-bedroom": {
|
|
ID: "light-bedroom", Name: "卧室灯", Type: plugin.DeviceLight,
|
|
State: map[string]any{"power": false, "brightness": 60, "color": "warm_white"},
|
|
Online: true,
|
|
},
|
|
"ac-livingroom": {
|
|
ID: "ac-livingroom", Name: "客厅空调", Type: plugin.DeviceAC,
|
|
State: map[string]any{"power": false, "temperature": 26, "mode": "cool"},
|
|
Online: true,
|
|
},
|
|
"ac-bedroom": {
|
|
ID: "ac-bedroom", Name: "卧室空调", Type: plugin.DeviceAC,
|
|
State: map[string]any{"power": false, "temperature": 25, "mode": "cool"},
|
|
Online: true,
|
|
},
|
|
"curtain-livingroom": {
|
|
ID: "curtain-livingroom", Name: "客厅窗帘", Type: plugin.DeviceCurtain,
|
|
State: map[string]any{"power": false, "position": 0},
|
|
Online: true,
|
|
},
|
|
"sensor-temperature": {
|
|
ID: "sensor-temperature", Name: "温度传感器", Type: plugin.DeviceSensor,
|
|
State: map[string]any{"temperature": 25.0, "unit": "celsius"},
|
|
Online: true,
|
|
},
|
|
"sensor-humidity": {
|
|
ID: "sensor-humidity", Name: "湿度传感器", Type: plugin.DeviceSensor,
|
|
State: map[string]any{"humidity": 55.0, "unit": "percent"},
|
|
Online: true,
|
|
},
|
|
"lock-door": {
|
|
ID: "lock-door", Name: "智能门锁", Type: plugin.DeviceLock,
|
|
State: map[string]any{"locked": true, "battery": 85},
|
|
Online: true,
|
|
},
|
|
},
|
|
subs: make([]chan plugin.DeviceEvent, 0),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// --- Plugin interface ---
|
|
|
|
func (p *Plugin) Name() string { return "iot-mock" }
|
|
func (p *Plugin) Version() string { return "1.0.0" }
|
|
func (p *Plugin) Description() string { return "模拟8个智能家居设备,仅供开发测试使用" }
|
|
func (p *Plugin) Enabled() bool { return true }
|
|
|
|
func (p *Plugin) Init(_ context.Context, _ plugin.PluginConfig) error {
|
|
// Start sensor fluctuation goroutine
|
|
go p.fluctuateSensors()
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) Close() error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.stopped {
|
|
return nil
|
|
}
|
|
p.stopped = true
|
|
close(p.stopCh)
|
|
for _, ch := range p.subs {
|
|
close(ch)
|
|
}
|
|
p.subs = nil
|
|
return nil
|
|
}
|
|
|
|
// --- IoTProvider interface ---
|
|
|
|
func (p *Plugin) ListDevices(_ context.Context) ([]plugin.Device, error) {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
result := make([]plugin.Device, 0, len(p.devices))
|
|
for _, d := range p.devices {
|
|
// Return a copy to prevent external mutation
|
|
clone := *d
|
|
clone.State = make(map[string]any, len(d.State))
|
|
for k, v := range d.State {
|
|
clone.State[k] = v
|
|
}
|
|
result = append(result, clone)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (p *Plugin) GetDevice(_ context.Context, id string) (*plugin.Device, error) {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
d, ok := p.devices[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("device %s not found", id)
|
|
}
|
|
clone := *d
|
|
clone.State = make(map[string]any, len(d.State))
|
|
for k, v := range d.State {
|
|
clone.State[k] = v
|
|
}
|
|
return &clone, nil
|
|
}
|
|
|
|
func (p *Plugin) SetDevice(_ context.Context, id, field string, value any) error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
d, ok := p.devices[id]
|
|
if !ok {
|
|
return fmt.Errorf("device %s not found", id)
|
|
}
|
|
|
|
oldValue := d.State[field]
|
|
d.State[field] = value
|
|
|
|
event := plugin.DeviceEvent{
|
|
DeviceID: id,
|
|
Field: field,
|
|
OldValue: oldValue,
|
|
NewValue: value,
|
|
Timestamp: time.Now().Unix(),
|
|
}
|
|
d.History = append(d.History, event)
|
|
// Keep only last 20 history events
|
|
if len(d.History) > 20 {
|
|
d.History = d.History[len(d.History)-20:]
|
|
}
|
|
|
|
// Broadcast to subscribers
|
|
p.broadcast(event)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) ToggleDevice(_ context.Context, id string) error {
|
|
p.mu.RLock()
|
|
d, ok := p.devices[id]
|
|
p.mu.RUnlock()
|
|
if !ok {
|
|
return fmt.Errorf("device %s not found", id)
|
|
}
|
|
|
|
currentPower, _ := d.State["power"].(bool)
|
|
return p.SetDevice(context.Background(), id, "power", !currentPower)
|
|
}
|
|
|
|
func (p *Plugin) SubscribeChanges(_ context.Context) (<-chan plugin.DeviceEvent, error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
ch := make(chan plugin.DeviceEvent, 32)
|
|
p.subs = append(p.subs, ch)
|
|
return ch, nil
|
|
}
|
|
|
|
// --- Internal ---
|
|
|
|
func (p *Plugin) broadcast(event plugin.DeviceEvent) {
|
|
for _, ch := range p.subs {
|
|
select {
|
|
case ch <- event:
|
|
default:
|
|
// Subscriber is too slow, drop event
|
|
}
|
|
}
|
|
}
|
|
|
|
// fluctuateSensors simulates temperature and humidity changes every 30 seconds.
|
|
func (p *Plugin) fluctuateSensors() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-p.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
// Fluctuate temperature sensor: ±0.2°C
|
|
p.fluctuateDeviceField("sensor-temperature", "temperature", 0.2)
|
|
|
|
// Fluctuate humidity sensor: ±1%
|
|
p.fluctuateDeviceField("sensor-humidity", "humidity", 1.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) fluctuateDeviceField(deviceID, field string, maxDelta float64) {
|
|
p.mu.Lock()
|
|
d, ok := p.devices[deviceID]
|
|
if !ok {
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
current, ok := d.State[field].(float64)
|
|
if !ok {
|
|
p.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
delta := (rand.Float64()*2 - 1) * maxDelta
|
|
newVal := current + delta
|
|
// Round to 1 decimal place
|
|
newVal = float64(int(newVal*10)) / 10
|
|
|
|
oldVal := d.State[field]
|
|
d.State[field] = newVal
|
|
p.mu.Unlock()
|
|
|
|
p.mu.RLock()
|
|
event := plugin.DeviceEvent{
|
|
DeviceID: deviceID,
|
|
Field: field,
|
|
OldValue: oldVal,
|
|
NewValue: newVal,
|
|
Timestamp: time.Now().Unix(),
|
|
}
|
|
p.mu.RUnlock()
|
|
|
|
p.broadcast(event)
|
|
}
|