package arlo import ( "context" "encoding/json" "fmt" "sync" log "github.com/sirupsen/logrus" ) // A Camera is a Device of type "camera". // This type is here just for semantics. Some methods explicitly require a device of a certain type. type Camera struct { arlo *Arlo Device CameraState stateMutex sync.RWMutex motionSubscribers []chan bool motionSubscribersMutex sync.RWMutex } type CameraState struct { InterfaceVersion int `json:"interfaceVersion"` SerialNumber string `json:"serialNumber"` BatteryLevel int `json:"batteryLevel"` BatteryTech string `json:"batteryTech"` ChargerTech string `json:"chargerTech"` ChargingState string `json:"chargingState"` ChargeOnly bool `json:"chargeOnly"` ChargeNotificationLedEnable bool `json:"chargeNotificationLedEnable"` AudioMicAGC int `json:"audioMicAGC"` SignalStrength int `json:"signalStrength"` Brightness int `json:"brightness"` Mirror bool `json:"mirror"` Flip bool `json:"flip"` PowerSaveMode int `json:"powerSaveMode"` Zoom struct { Topleftx int `json:"topleftx"` Toplefty int `json:"toplefty"` Bottomrightx int `json:"bottomrightx"` Bottomrighty int `json:"bottomrighty"` } `json:"zoom"` Mic struct { Mute bool `json:"mute"` Volume int `json:"volume"` } `json:"mic"` Speaker struct { Mute bool `json:"mute"` Volume int `json:"volume"` } `json:"speaker"` StreamingMode string `json:"streamingMode"` ContinuousStreamState string `json:"continuousStreamState"` Motion struct { Sensitivity int `json:"sensitivity"` Zones []interface{} `json:"zones"` } `json:"motion"` Resolution struct { Width int `json:"width"` Height int `json:"height"` } `json:"resolution"` IdleLedEnable bool `json:"idleLedEnable"` PrivacyActive bool `json:"privacyActive"` StandbyActive bool `json:"standbyActive"` SetupActive bool `json:"setupActive"` ConnectionState string `json:"connectionState"` ActivityState string `json:"activityState"` SwVersion string `json:"swVersion"` HwVersion string `json:"hwVersion"` ModelID string `json:"modelId"` MotionSetupModeEnabled bool `json:"motionSetupModeEnabled"` MotionSetupModeSensitivity int `json:"motionSetupModeSensitivity"` MotionDetected bool `json:"motionDetected"` AudioDetected bool `json:"audioDetected"` HasStreamed bool `json:"hasStreamed"` LocalRecordingActive bool `json:"localRecordingActive"` OlsonTimeZone string `json:"olsonTimeZone"` Name string `json:"name"` NightVisionMode int `json:"nightVisionMode"` VideoMode string `json:"videoMode"` Hdr string `json:"hdr"` UpdateAvailable interface{} `json:"updateAvailable"` BlockNotifications struct { Block bool `json:"block"` Duration int `json:"duration"` EndTime int `json:"endTime"` } `json:"blockNotifications"` BestLocalLiveStreaming string `json:"bestLocalLiveStreaming"` } func NewCamera(arlo *Arlo, device Device) *Camera { return &Camera{ arlo: arlo, Device: device, } } func (c *Camera) On(ctx context.Context) error { err := c.arlo.makeRequest(ctx, c.DeviceId, c.XCloudId, "set", fmt.Sprintf("cameras/%s", c.DeviceId), true, CameraProperties{ PrivacyActive: false, }, nil) if err != nil { return err } return nil } // On turns a camera off; meaning it won't detect and record events. func (c *Camera) Off(ctx context.Context) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), PublishResponse: true, Properties: CameraProperties{ PrivacyActive: true, }, From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), To: c.DeviceId, } return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId) } // SetBrightness sets the camera brightness. // NOTE: Brightness is between -2 and 2 in increments of 1 (-2, -1, 0, 1, 2). // Setting it to an invalid value has no effect. func (c *Camera) SetBrightness(ctx context.Context, brightness int) (response *EventStreamResponse, err error) { // Sanity check; if the values are above or below the allowed limits, set them to their limit. if brightness < -2 { brightness = -2 } else if brightness > 2 { brightness = 2 } payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), PublishResponse: true, Properties: CameraProperties{ Brightness: brightness, }, From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), To: c.DeviceId, } return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId) } func (c *Camera) EnableMotionAlerts(ctx context.Context, sensitivity int, zones []string) error { err := c.arlo.makeRequest(ctx, c.DeviceId, c.XCloudId, "set", fmt.Sprintf("cameras/%s", c.DeviceId), true, MotionDetectionProperties{ BaseDetectionProperties: BaseDetectionProperties{ Armed: true, Sensitivity: sensitivity, Zones: zones, }, }, nil) if err != nil { return err } return nil } func (c *Camera) DisableMotionAlerts(ctx context.Context, sensitivity int, zones []string) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), PublishResponse: true, Properties: MotionDetectionProperties{ BaseDetectionProperties: BaseDetectionProperties{ Armed: false, Sensitivity: sensitivity, Zones: zones, }, }, From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), To: c.DeviceId, } return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId) } func (c *Camera) EnableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), PublishResponse: true, Properties: AudioDetectionProperties{ BaseDetectionProperties: BaseDetectionProperties{ Armed: true, Sensitivity: sensitivity, }, }, From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), To: c.ParentId, } return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId) } func (c *Camera) DisableAudioAlerts(ctx context.Context, sensitivity int) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), PublishResponse: true, Properties: AudioDetectionProperties{ BaseDetectionProperties: BaseDetectionProperties{ Armed: false, Sensitivity: sensitivity, }, }, From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), To: c.DeviceId, } return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId) } // action: disabled OR recordSnapshot OR recordVideo func (c *Camera) SetAlertNotificationMethods(ctx context.Context, action string, email, push bool) (response *EventStreamResponse, err error) { payload := EventStreamPayload{ Action: "set", Resource: fmt.Sprintf("cameras/%s", c.DeviceId), PublishResponse: true, Properties: EventActionProperties{ BaseEventActionProperties: BaseEventActionProperties{ ActionType: action, StopType: "timeout", Timeout: 15, EmailNotification: EmailNotification{ Enabled: email, EmailList: []string{"__OWNER_EMAIL__"}, PushNotification: push, }, }, }, From: fmt.Sprintf("%s_%s", c.UserId, TransIdPrefix), To: c.DeviceId, } return c.arlo.makeEventStreamRequest(ctx, payload, c.XCloudId) } func (c *Camera) subscribeToStateUpdate() { go func() { respChan := c.arlo.eventStream.subscribeResource(fmt.Sprintf("cameras/%s", c.DeviceId)) for msg := range respChan { c.stateMutex.Lock() old := *c err := json.Unmarshal(msg.RawProperties, &c) if err != nil { log.Errorf("camera state update > unmarshalling properties: %v", err) continue } c.stateMutex.Unlock() c.processStateChanges(old, *c) } }() } func (c *Camera) RefreshState(ctx context.Context) error { old := *c err := c.arlo.makeRequest(ctx, c.DeviceId, c.XCloudId, "get", fmt.Sprintf("cameras/%s", c.DeviceId), false, nil, &c) if err != nil { return err } c.processStateChanges(old, *c) return nil } func (c *Camera) processStateChanges(old Camera, new Camera) { if old.MotionDetected != new.MotionDetected { c.motionSubscribersMutex.RLock() for _, subscriber := range c.motionSubscribers { subscriber <- new.MotionDetected } c.motionSubscribersMutex.RUnlock() } } func (c *Camera) SubscribeToMotion() chan bool { c.motionSubscribersMutex.Lock() defer c.motionSubscribersMutex.Unlock() out := make(chan bool) c.motionSubscribers = append(c.motionSubscribers, out) return out }