// 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 ProfileToken string PathName string Generation int64 ResourceVersion 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) }