From 147231e4fd50b74f9d7e0b564915760685941d71 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 30 Oct 2021 14:38:14 +0200 Subject: [PATCH] add sensor scenes for Motion and Temperature. --- app/services/events.go | 26 ++++++++- app/services/publisher/publisher.go | 15 ++++++ app/services/publisher/scene.go | 43 ++++++++++++++- internal/drivers/hue/state.go | 2 + models/device.go | 20 +++---- models/event.go | 1 - models/scene.go | 81 +++++++++++++++++++++++++---- 7 files changed, 165 insertions(+), 23 deletions(-) diff --git a/app/services/events.go b/app/services/events.go index 9e86300..bd4d7f0 100644 --- a/app/services/events.go +++ b/app/services/events.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "git.aiterp.net/lucifer/new-server/app/config" + "git.aiterp.net/lucifer/new-server/app/services/publisher" "git.aiterp.net/lucifer/new-server/models" "log" "math" @@ -220,7 +221,30 @@ func handleSpecial(event models.Event) error { } return driver.Publish(ctx, bridge, devices) + case models.ENSensorPresenceStarted, models.ENSensorPresenceEnded, models.ENSensorTemperature: + id, _ := strconv.Atoi(event.Payload["deviceId"]) + updateTime, _ := strconv.ParseInt(event.Payload["lastUpdated"], 10, 64) + state := models.SceneSensor{ + ID: id, + UpdateTime: time.Unix(updateTime, 0), + } + + switch event.Name { + case models.ENSensorPresenceStarted: + presence := true + state.Presence = &presence + case models.ENSensorPresenceEnded: + presence := false + state.Presence = &presence + case models.ENSensorTemperature: + temperature, _ := strconv.ParseFloat(event.Payload["temperature"], 64) + state.Temperature = &temperature + } + + publisher.Global().UpdateSensor(state) + + return nil default: return nil } -} +} \ No newline at end of file diff --git a/app/services/publisher/publisher.go b/app/services/publisher/publisher.go index 2550e84..a0f315c 100644 --- a/app/services/publisher/publisher.go +++ b/app/services/publisher/publisher.go @@ -17,6 +17,7 @@ type Publisher struct { started map[int]bool pending map[int][]models.Device waiting map[int]chan struct{} + sensorCache map[int]models.SceneSensor } func (p *Publisher) SceneState(deviceID int) *models.DeviceState { @@ -42,6 +43,15 @@ func (p *Publisher) UpdateScene(data models.Scene) { p.mu.Unlock() } +func (p *Publisher) UpdateSensor(data models.SceneSensor) { + p.mu.Lock() + if assignment := p.sceneAssignment[data.ID]; assignment != nil { + assignment.UpdateSensor(data) + } + p.sensorCache[data.ID] = data + p.mu.Unlock() +} + func (p *Publisher) ReloadScenes(ctx context.Context) error { scenes, err := config.SceneRepository().FetchAll(ctx) if err != nil { @@ -90,6 +100,10 @@ func (p *Publisher) Publish(devices ...models.Device) { if p.sceneAssignment[device.ID] != nil { p.sceneAssignment[device.ID].UpsertDevice(device) + + if cache, ok := p.sensorCache[device.ID]; ok { + p.sceneAssignment[device.ID].UpdateSensor(cache) + } } else { p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device) if p.waiting[device.BridgeID] != nil { @@ -291,6 +305,7 @@ var publisher = Publisher{ started: make(map[int]bool), pending: make(map[int][]models.Device), waiting: make(map[int]chan struct{}), + sensorCache: make(map[int]models.SceneSensor), } func Global() *Publisher { diff --git a/app/services/publisher/scene.go b/app/services/publisher/scene.go index e6d605d..f4a00bb 100644 --- a/app/services/publisher/scene.go +++ b/app/services/publisher/scene.go @@ -13,11 +13,25 @@ type Scene struct { roleMap map[int]int roleList map[int][]models.Device lastStates map[int]models.DeviceState + sensors []models.SceneSensor due bool lastInterval int64 } +func (s *Scene) UpdateSensor(data models.SceneSensor) { + s.due = true + + for i, sensor := range s.sensors { + if sensor.ID == data.ID { + s.sensors[i] = data + return + } + } + + s.sensors = append(s.sensors, data) +} + // UpdateScene updates the scene data and re-seats all devices. func (s *Scene) UpdateScene(data models.Scene) { devices := make([]models.Device, 0, 16) @@ -86,6 +100,13 @@ func (s *Scene) RemoveDevice(device models.Device) { delete(s.roleMap, device.ID) delete(s.lastStates, device.ID) + + for i, sensor := range s.sensors { + if sensor.ID == device.ID { + s.sensors = append(s.sensors[:i], s.sensors[i+1:]...) + break + } + } } func (s *Scene) Empty() bool { @@ -130,6 +151,7 @@ func (s *Scene) Run() []models.Device { return []models.Device{} } + currentTime := time.Now() intervalNumber := int64(0) intervalMax := int64(1) if s.data.IntervalMS > 0 { @@ -149,14 +171,31 @@ func (s *Scene) Run() []models.Device { continue } + sensorCount := 0 + for _, device := range list { + if device.IsOnlySensor() { + sensorCount += 1 + } + } + role := s.data.Roles[i] + jSkip := 0 + for j, device := range list { + if device.IsOnlySensor() { + updatedDevices = append(updatedDevices, device) + jSkip += 1 + continue + } + newState := role.ApplyEffect(&device, models.SceneRunContext{ - Index: j, - Length: len(list), + CurrentTime: currentTime, + Index: j - jSkip, + Length: len(list) - sensorCount, IntervalNumber: intervalNumber, IntervalMax: intervalMax, + Sensors: s.sensors, }) err := device.SetState(newState) diff --git a/internal/drivers/hue/state.go b/internal/drivers/hue/state.go index 2e89698..3d3a46f 100644 --- a/internal/drivers/hue/state.go +++ b/internal/drivers/hue/state.go @@ -219,6 +219,7 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event { "deviceInternalId": newData.UniqueID, "minutesElapsed": strconv.Itoa(state.presenceCooldown - 1), "secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60), + "lastUpdated": strconv.FormatInt(stateTime.Unix(), 10), }, } } @@ -235,6 +236,7 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event { "temperature": strconv.FormatFloat(float64(newData.State.Temperature)/100, 'f', 2, 64), "deviceId": strconv.Itoa(state.externalID), "deviceInternalId": newData.UniqueID, + "lastUpdated": strconv.FormatInt(stateTime.Unix(), 10), }, } } diff --git a/models/device.go b/models/device.go index aa76fe2..446569a 100644 --- a/models/device.go +++ b/models/device.go @@ -2,7 +2,6 @@ package models import ( "context" - "math" "strings" "time" ) @@ -156,6 +155,17 @@ func (d *Device) HasTag(tags ...string) bool { return false } +func (d *Device) IsOnlySensor() bool { + return !d.HasCapability( + DCPower, + DCColorHS, + DCColorHSK, + DCColorKelvin, + DCIntensity, + DCTemperatureControl, + ) +} + func (d *Device) HasCapability(capabilities ...DeviceCapability) bool { for _, c := range d.Capabilities { for _, c2 := range capabilities { @@ -198,14 +208,6 @@ func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState { intensity := device.State.Intensity * *s.Intensity n.Intensity = &intensity } - if s.Color != nil { - c, err := ParseColorValue(*s.Color) - if err == nil { - c.Hue = math.Mod(device.State.Color.Hue+c.Hue, 360) - c.Saturation *= device.State.Color.Saturation - c.Kelvin += device.State.Color.Kelvin - } - } return n } diff --git a/models/event.go b/models/event.go index 6101bb0..c2294e3 100644 --- a/models/event.go +++ b/models/event.go @@ -24,7 +24,6 @@ var ( ENBridgeDisconnected = "BridgeDisconnected" ENButtonPressed = "ButtonPressed" ENSensorPresenceStarted = "SensorPresenceStarted" - ENSensorPresenceEnding = "SensorPresenceEnding" ENSensorPresenceEnded = "SensorPresenceEnded" ENSensorTemperature = "SensorTemperature" ) diff --git a/models/scene.go b/models/scene.go index 7dd215e..09cf68c 100644 --- a/models/scene.go +++ b/models/scene.go @@ -61,24 +61,38 @@ const ( SEGradient SceneEffect = "Gradient" SEWalkingGradient SceneEffect = "WalkingGradient" SETransition SceneEffect = "Transition" + SEMotion SceneEffect = "Motion" + SETemperature SceneEffect = "Temperature" ) type SceneRole struct { - Effect SceneEffect `json:"effect"` - PowerMode ScenePowerMode `json:"powerMode"` - TargetKind ReferenceKind `json:"targetKind"` - TargetValue string `json:"targetValue"` - Interpolate bool `json:"interpolate"` - Relative bool `json:"relative"` - Order string `json:"order"` - States []NewDeviceState `json:"states"` + Effect SceneEffect `json:"effect"` + MotionSeconds float64 `json:"motionSeconds"` + MinTemperature int `json:"minTemperature"` + MaxTemperature int `json:"maxTemperature"` + PowerMode ScenePowerMode `json:"powerMode"` + TargetKind ReferenceKind `json:"targetKind"` + TargetValue string `json:"targetValue"` + Interpolate bool `json:"interpolate"` + Relative bool `json:"relative"` + Order string `json:"order"` + States []NewDeviceState `json:"states"` } type SceneRunContext struct { + CurrentTime time.Time Index int Length int IntervalNumber int64 IntervalMax int64 + Sensors []SceneSensor +} + +type SceneSensor struct { + ID int + UpdateTime time.Time + Temperature *float64 + Presence *bool } func (d *SceneRunContext) PositionFacShifted() float64 { @@ -120,7 +134,7 @@ func (r *SceneRole) Validate() error { } switch r.Effect { - case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition: + case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature: default: return ErrSceneRoleUnknownEffect } @@ -178,6 +192,53 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New newState = r.State(c.PositionFacShifted()) case SETransition: newState = r.State(c.IntervalFac()) + case SEMotion: + presence := false + absenceTime := time.Time{} + + for _, sensors := range c.Sensors { + if sensors.Presence == nil { + continue + } + + if *sensors.Presence { + presence = true + } else { + if sensors.UpdateTime.After(absenceTime) { + absenceTime = sensors.UpdateTime + } + } + } + + if presence { + newState = r.State(0.0) + } else { + fac := c.CurrentTime.Sub(absenceTime).Seconds() / r.MotionSeconds + if fac < 0 { + fac = 0 + } else if fac > 1 { + fac = 1 + } + + newState = r.State(fac) + } + case SETemperature: + avg := 0.0 + count := 0 + + for _, sensor := range c.Sensors { + if sensor.Temperature == nil { + continue + } + + if count == 0 { + avg = *sensor.Temperature + count = 1 + } else { + avg = ((avg * float64(count)) + *sensor.Temperature) / float64(count+1) + count += 1 + } + } } if r.Relative { @@ -188,7 +249,7 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New case SPDevice: newState.Power = nil case SPScene: - // Do nothing + // Do nothing case SPBoth: if newState.Power != nil { powerIntersection := *newState.Power && device.State.Power