Files
yovision/Sense/internal/device/model.go
T
QiuSW 36786723e3
Harness governance / validate (push) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
feat(store): add Area admission and audit outbox [T-010]
2026-08-07 18:23:24 +08:00

183 lines
5.3 KiB
Go

// Package device contains the Sense device-ledger domain model.
package device
import (
"errors"
"fmt"
"net/url"
"slices"
"strings"
"time"
)
const (
DefaultVideoChannels = 16
MaximumVideoChannels = 128
)
type Modality string
const (
ModalityVideo Modality = "video"
ModalityRadar Modality = "radar"
ModalityContact Modality = "contact"
ModalityButton Modality = "button"
ModalityWearable Modality = "wearable"
ModalityOther Modality = "other"
)
type Capability string
const (
CapabilityVideoCapture Capability = "video_capture"
CapabilityAudioCapture Capability = "audio_capture"
CapabilitySpatialRule Capability = "spatial_rule"
CapabilityTelemetry Capability = "telemetry"
)
type DesiredState string
const (
DesiredDisabled DesiredState = "disabled"
DesiredEnabled DesiredState = "enabled"
)
type ActualState string
const (
ActualPending ActualState = "pending"
ActualOnline ActualState = "online"
ActualOffline ActualState = "offline"
ActualFailed ActualState = "failed"
)
type Site struct {
TenantID string
ID string
Name string
MaxVideoChannels int
}
func (s *Site) ApplyDefaults() {
if s.MaxVideoChannels == 0 {
s.MaxVideoChannels = DefaultVideoChannels
}
}
func (s Site) Validate() error {
if strings.TrimSpace(s.TenantID) == "" || strings.TrimSpace(s.ID) == "" {
return errors.New("tenant ID and site ID are required")
}
if s.MaxVideoChannels < 1 || s.MaxVideoChannels > MaximumVideoChannels {
return fmt.Errorf("max video channels must be between 1 and %d", MaximumVideoChannels)
}
return nil
}
type Device struct {
ID string
TenantID string
SiteID string
AreaID string
SerialNumber string
Name string
Modality Modality
Capabilities []Capability
DesiredState DesiredState
ActualState ActualState
EndpointRef string
CredentialRef string
PathName string
Generation int64
QuotaSourceVersion int64
AreaPolicySourceVersion int64
CreatedAt time.Time
UpdatedAt time.Time
}
func (d Device) Validate() error {
if strings.TrimSpace(d.ID) == "" || strings.TrimSpace(d.TenantID) == "" || strings.TrimSpace(d.SiteID) == "" {
return errors.New("device ID, tenant ID and site ID are required")
}
if strings.TrimSpace(d.SerialNumber) == "" || strings.TrimSpace(d.Name) == "" {
return errors.New("serial number and device name are required")
}
if !validModality(d.Modality) {
return fmt.Errorf("unsupported modality %q", d.Modality)
}
if d.DesiredState != DesiredEnabled && d.DesiredState != DesiredDisabled {
return fmt.Errorf("unsupported desired state %q", d.DesiredState)
}
if d.ActualState != ActualPending && d.ActualState != ActualOnline && d.ActualState != ActualOffline && d.ActualState != ActualFailed {
return fmt.Errorf("unsupported actual state %q", d.ActualState)
}
if d.DesiredState == DesiredEnabled && d.HasCapability(CapabilityVideoCapture) {
if strings.TrimSpace(d.EndpointRef) == "" || strings.TrimSpace(d.PathName) == "" {
return errors.New("enabled video devices require endpoint ref and path name")
}
}
if d.EndpointRef != "" {
endpoint, err := url.Parse(d.EndpointRef)
if err != nil || endpoint.Scheme == "" {
return errors.New("endpoint ref must be an absolute URI")
}
if endpoint.User != nil {
return errors.New("endpoint ref must not contain credentials")
}
}
if d.PathName != "" && !validPathName(d.PathName) {
return errors.New("path name must contain safe ASCII segments")
}
seen := make(map[Capability]struct{}, len(d.Capabilities))
for _, capability := range d.Capabilities {
if !validCapability(capability) {
return fmt.Errorf("unsupported capability %q", capability)
}
if _, ok := seen[capability]; ok {
return fmt.Errorf("duplicate capability %q", capability)
}
seen[capability] = struct{}{}
}
return nil
}
func (d Device) HasCapability(capability Capability) bool {
return slices.Contains(d.Capabilities, capability)
}
func (d Device) ConsumesVideoChannel() bool {
return d.DesiredState == DesiredEnabled && d.HasCapability(CapabilityVideoCapture)
}
func validModality(value Modality) bool {
return slices.Contains([]Modality{ModalityVideo, ModalityRadar, ModalityContact, ModalityButton, ModalityWearable, ModalityOther}, value)
}
func validCapability(value Capability) bool {
return slices.Contains([]Capability{CapabilityVideoCapture, CapabilityAudioCapture, CapabilitySpatialRule, CapabilityTelemetry}, value)
}
func validPathName(value string) bool {
if strings.HasPrefix(value, "/") || strings.HasSuffix(value, "/") || strings.Contains(value, "//") || strings.Contains(value, "..") {
return false
}
for _, character := range value {
if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') ||
(character >= '0' && character <= '9') || strings.ContainsRune("-_/.", character) {
continue
}
return false
}
return value != ""
}
type QuotaExceededError struct {
TenantID string
SiteID string
Limit int
}
func (e *QuotaExceededError) Error() string {
return fmt.Sprintf("video channel quota exceeded for site %s/%s (limit %d)", e.TenantID, e.SiteID, e.Limit)
}