mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-10 07:25:37 -05:00
switch hto monorepo structure
This commit is contained in:
88
backend/internal/server/wayland/gamma.go
Normal file
88
backend/internal/server/wayland/gamma.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/utils"
|
||||
)
|
||||
|
||||
type GammaRamp struct {
|
||||
Red []uint16
|
||||
Green []uint16
|
||||
Blue []uint16
|
||||
}
|
||||
|
||||
func GenerateGammaRamp(size uint32, temp int, gamma float64) GammaRamp {
|
||||
ramp := GammaRamp{
|
||||
Red: make([]uint16, size),
|
||||
Green: make([]uint16, size),
|
||||
Blue: make([]uint16, size),
|
||||
}
|
||||
|
||||
for i := uint32(0); i < size; i++ {
|
||||
val := float64(i) / float64(size-1)
|
||||
|
||||
valGamma := math.Pow(val, 1.0/gamma)
|
||||
|
||||
r, g, b := temperatureToRGB(temp)
|
||||
|
||||
ramp.Red[i] = uint16(utils.Clamp(valGamma*r*65535.0, 0, 65535))
|
||||
ramp.Green[i] = uint16(utils.Clamp(valGamma*g*65535.0, 0, 65535))
|
||||
ramp.Blue[i] = uint16(utils.Clamp(valGamma*b*65535.0, 0, 65535))
|
||||
}
|
||||
|
||||
return ramp
|
||||
}
|
||||
|
||||
func GenerateIdentityRamp(size uint32) GammaRamp {
|
||||
ramp := GammaRamp{
|
||||
Red: make([]uint16, size),
|
||||
Green: make([]uint16, size),
|
||||
Blue: make([]uint16, size),
|
||||
}
|
||||
|
||||
for i := uint32(0); i < size; i++ {
|
||||
val := uint16((float64(i) / float64(size-1)) * 65535.0)
|
||||
ramp.Red[i] = val
|
||||
ramp.Green[i] = val
|
||||
ramp.Blue[i] = val
|
||||
}
|
||||
|
||||
return ramp
|
||||
}
|
||||
|
||||
func temperatureToRGB(temp int) (float64, float64, float64) {
|
||||
tempK := float64(temp) / 100.0
|
||||
|
||||
var r, g, b float64
|
||||
|
||||
if tempK <= 66 {
|
||||
r = 1.0
|
||||
} else {
|
||||
r = tempK - 60
|
||||
r = 329.698727446 * math.Pow(r, -0.1332047592)
|
||||
r = utils.Clamp(r, 0, 255) / 255.0
|
||||
}
|
||||
|
||||
if tempK <= 66 {
|
||||
g = tempK
|
||||
g = 99.4708025861*math.Log(g) - 161.1195681661
|
||||
g = utils.Clamp(g, 0, 255) / 255.0
|
||||
} else {
|
||||
g = tempK - 60
|
||||
g = 288.1221695283 * math.Pow(g, -0.0755148492)
|
||||
g = utils.Clamp(g, 0, 255) / 255.0
|
||||
}
|
||||
|
||||
if tempK >= 66 {
|
||||
b = 1.0
|
||||
} else if tempK <= 19 {
|
||||
b = 0.0
|
||||
} else {
|
||||
b = tempK - 10
|
||||
b = 138.5177312231*math.Log(b) - 305.0447927307
|
||||
b = utils.Clamp(b, 0, 255) / 255.0
|
||||
}
|
||||
|
||||
return r, g, b
|
||||
}
|
||||
120
backend/internal/server/wayland/gamma_test.go
Normal file
120
backend/internal/server/wayland/gamma_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/utils"
|
||||
)
|
||||
|
||||
func TestGenerateGammaRamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size uint32
|
||||
temp int
|
||||
gamma float64
|
||||
}{
|
||||
{"small_warm", 16, 6500, 1.0},
|
||||
{"small_cool", 16, 4000, 1.0},
|
||||
{"large_warm", 256, 6500, 1.0},
|
||||
{"large_cool", 256, 4000, 1.0},
|
||||
{"custom_gamma", 64, 5500, 1.2},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ramp := GenerateGammaRamp(tt.size, tt.temp, tt.gamma)
|
||||
|
||||
if len(ramp.Red) != int(tt.size) {
|
||||
t.Errorf("expected %d red values, got %d", tt.size, len(ramp.Red))
|
||||
}
|
||||
if len(ramp.Green) != int(tt.size) {
|
||||
t.Errorf("expected %d green values, got %d", tt.size, len(ramp.Green))
|
||||
}
|
||||
if len(ramp.Blue) != int(tt.size) {
|
||||
t.Errorf("expected %d blue values, got %d", tt.size, len(ramp.Blue))
|
||||
}
|
||||
|
||||
if ramp.Red[0] != 0 || ramp.Green[0] != 0 || ramp.Blue[0] != 0 {
|
||||
t.Errorf("first values should be 0, got R:%d G:%d B:%d",
|
||||
ramp.Red[0], ramp.Green[0], ramp.Blue[0])
|
||||
}
|
||||
|
||||
lastIdx := tt.size - 1
|
||||
if ramp.Red[lastIdx] == 0 || ramp.Green[lastIdx] == 0 || ramp.Blue[lastIdx] == 0 {
|
||||
t.Errorf("last values should be non-zero, got R:%d G:%d B:%d",
|
||||
ramp.Red[lastIdx], ramp.Green[lastIdx], ramp.Blue[lastIdx])
|
||||
}
|
||||
|
||||
for i := uint32(1); i < tt.size; i++ {
|
||||
if ramp.Red[i] < ramp.Red[i-1] {
|
||||
t.Errorf("red ramp not monotonic at index %d", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemperatureToRGB(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
temp int
|
||||
}{
|
||||
{"very_warm", 6500},
|
||||
{"neutral", 5500},
|
||||
{"cool", 4000},
|
||||
{"very_cool", 3000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r, g, b := temperatureToRGB(tt.temp)
|
||||
|
||||
if r < 0 || r > 1 {
|
||||
t.Errorf("red out of range: %f", r)
|
||||
}
|
||||
if g < 0 || g > 1 {
|
||||
t.Errorf("green out of range: %f", g)
|
||||
}
|
||||
if b < 0 || b > 1 {
|
||||
t.Errorf("blue out of range: %f", b)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemperatureProgression(t *testing.T) {
|
||||
temps := []int{3000, 4000, 5000, 6000, 6500}
|
||||
|
||||
var prevBlue float64
|
||||
for i, temp := range temps {
|
||||
_, _, b := temperatureToRGB(temp)
|
||||
if i > 0 && b < prevBlue {
|
||||
t.Errorf("blue should increase with temperature, %d->%d: %f->%f",
|
||||
temps[i-1], temp, prevBlue, b)
|
||||
}
|
||||
prevBlue = b
|
||||
}
|
||||
}
|
||||
|
||||
func TestClamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
val float64
|
||||
min float64
|
||||
max float64
|
||||
expected float64
|
||||
}{
|
||||
{5, 0, 10, 5},
|
||||
{-5, 0, 10, 0},
|
||||
{15, 0, 10, 10},
|
||||
{0, 0, 10, 0},
|
||||
{10, 0, 10, 10},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := utils.Clamp(tt.val, tt.min, tt.max)
|
||||
if result != tt.expected {
|
||||
t.Errorf("clamp(%f, %f, %f) = %f, want %f",
|
||||
tt.val, tt.min, tt.max, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
50
backend/internal/server/wayland/geolocation.go
Normal file
50
backend/internal/server/wayland/geolocation.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/log"
|
||||
)
|
||||
|
||||
type ipAPIResponse struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
func FetchIPLocation() (*float64, *float64, error) {
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get("http://ip-api.com/json/")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch IP location: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var data ipAPIResponse
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if data.Lat == 0 && data.Lon == 0 {
|
||||
return nil, nil, fmt.Errorf("missing location data in response")
|
||||
}
|
||||
|
||||
log.Infof("Fetched IP-based location: %s (%.4f, %.4f)", data.City, data.Lat, data.Lon)
|
||||
return &data.Lat, &data.Lon, nil
|
||||
}
|
||||
205
backend/internal/server/wayland/handlers.go
Normal file
205
backend/internal/server/wayland/handlers.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/server/models"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type SuccessResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
|
||||
if manager == nil {
|
||||
models.RespondError(conn, req.ID, "wayland manager not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "wayland.gamma.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "wayland.gamma.setTemperature":
|
||||
handleSetTemperature(conn, req, manager)
|
||||
case "wayland.gamma.setLocation":
|
||||
handleSetLocation(conn, req, manager)
|
||||
case "wayland.gamma.setManualTimes":
|
||||
handleSetManualTimes(conn, req, manager)
|
||||
case "wayland.gamma.setUseIPLocation":
|
||||
handleSetUseIPLocation(conn, req, manager)
|
||||
case "wayland.gamma.setGamma":
|
||||
handleSetGamma(conn, req, manager)
|
||||
case "wayland.gamma.setEnabled":
|
||||
handleSetEnabled(conn, req, manager)
|
||||
case "wayland.gamma.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req Request, manager *Manager) {
|
||||
state := manager.GetState()
|
||||
models.Respond(conn, req.ID, state)
|
||||
}
|
||||
|
||||
func handleSetTemperature(conn net.Conn, req Request, manager *Manager) {
|
||||
var lowTemp, highTemp int
|
||||
|
||||
if temp, ok := req.Params["temp"].(float64); ok {
|
||||
lowTemp = int(temp)
|
||||
highTemp = int(temp)
|
||||
} else {
|
||||
low, okLow := req.Params["low"].(float64)
|
||||
high, okHigh := req.Params["high"].(float64)
|
||||
|
||||
if !okLow || !okHigh {
|
||||
models.RespondError(conn, req.ID, "missing temperature parameters (provide 'temp' or both 'low' and 'high')")
|
||||
return
|
||||
}
|
||||
|
||||
lowTemp = int(low)
|
||||
highTemp = int(high)
|
||||
}
|
||||
|
||||
if err := manager.SetTemperature(lowTemp, highTemp); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "temperature set"})
|
||||
}
|
||||
|
||||
func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
|
||||
lat, ok := req.Params["latitude"].(float64)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'latitude' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
lon, ok := req.Params["longitude"].(float64)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'longitude' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetLocation(lat, lon); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"})
|
||||
}
|
||||
|
||||
func handleSetManualTimes(conn net.Conn, req Request, manager *Manager) {
|
||||
sunriseParam := req.Params["sunrise"]
|
||||
sunsetParam := req.Params["sunset"]
|
||||
|
||||
if sunriseParam == nil || sunsetParam == nil {
|
||||
manager.ClearManualTimes()
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"})
|
||||
return
|
||||
}
|
||||
|
||||
sunriseStr, ok := sunriseParam.(string)
|
||||
if !ok || sunriseStr == "" {
|
||||
manager.ClearManualTimes()
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"})
|
||||
return
|
||||
}
|
||||
|
||||
sunsetStr, ok := sunsetParam.(string)
|
||||
if !ok || sunsetStr == "" {
|
||||
manager.ClearManualTimes()
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times cleared"})
|
||||
return
|
||||
}
|
||||
|
||||
sunrise, err := time.Parse("15:04", sunriseStr)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, "invalid sunrise format (use HH:MM)")
|
||||
return
|
||||
}
|
||||
|
||||
sunset, err := time.Parse("15:04", sunsetStr)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, "invalid sunset format (use HH:MM)")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetManualTimes(sunrise, sunset); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "manual times set"})
|
||||
}
|
||||
|
||||
func handleSetUseIPLocation(conn net.Conn, req Request, manager *Manager) {
|
||||
use, ok := req.Params["use"].(bool)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'use' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetUseIPLocation(use)
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "IP location preference set"})
|
||||
}
|
||||
|
||||
func handleSetGamma(conn net.Conn, req Request, manager *Manager) {
|
||||
gamma, ok := req.Params["gamma"].(float64)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'gamma' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetGamma(gamma); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "gamma set"})
|
||||
}
|
||||
|
||||
func handleSetEnabled(conn net.Conn, req Request, manager *Manager) {
|
||||
enabled, ok := req.Params["enabled"].(bool)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'enabled' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
manager.SetEnabled(enabled)
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "enabled state set"})
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
|
||||
initialState := manager.GetState()
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
ID: req.ID,
|
||||
Result: &initialState,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range stateChan {
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||
Result: &state,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
1367
backend/internal/server/wayland/manager.go
Normal file
1367
backend/internal/server/wayland/manager.go
Normal file
File diff suppressed because it is too large
Load Diff
86
backend/internal/server/wayland/suncalc.go
Normal file
86
backend/internal/server/wayland/suncalc.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
degToRad = math.Pi / 180.0
|
||||
radToDeg = 180.0 / math.Pi
|
||||
solarNoon = 12.0
|
||||
sunriseAngle = -0.833
|
||||
)
|
||||
|
||||
func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes {
|
||||
utcDate := date.UTC()
|
||||
year, month, day := utcDate.Date()
|
||||
loc := date.Location()
|
||||
|
||||
dayOfYear := utcDate.YearDay()
|
||||
|
||||
gamma := 2 * math.Pi / 365 * float64(dayOfYear-1)
|
||||
|
||||
eqTime := 229.18 * (0.000075 +
|
||||
0.001868*math.Cos(gamma) -
|
||||
0.032077*math.Sin(gamma) -
|
||||
0.014615*math.Cos(2*gamma) -
|
||||
0.040849*math.Sin(2*gamma))
|
||||
|
||||
decl := 0.006918 -
|
||||
0.399912*math.Cos(gamma) +
|
||||
0.070257*math.Sin(gamma) -
|
||||
0.006758*math.Cos(2*gamma) +
|
||||
0.000907*math.Sin(2*gamma) -
|
||||
0.002697*math.Cos(3*gamma) +
|
||||
0.00148*math.Sin(3*gamma)
|
||||
|
||||
latRad := lat * degToRad
|
||||
|
||||
cosHourAngle := (math.Sin(sunriseAngle*degToRad) -
|
||||
math.Sin(latRad)*math.Sin(decl)) /
|
||||
(math.Cos(latRad) * math.Cos(decl))
|
||||
|
||||
if cosHourAngle > 1 {
|
||||
return SunTimes{
|
||||
Sunrise: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc),
|
||||
Sunset: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc),
|
||||
}
|
||||
}
|
||||
if cosHourAngle < -1 {
|
||||
return SunTimes{
|
||||
Sunrise: time.Date(year, month, day, 0, 0, 0, 0, time.UTC).In(loc),
|
||||
Sunset: time.Date(year, month, day, 23, 59, 59, 0, time.UTC).In(loc),
|
||||
}
|
||||
}
|
||||
|
||||
hourAngle := math.Acos(cosHourAngle) * radToDeg
|
||||
|
||||
sunriseTime := solarNoon - hourAngle/15.0 - lon/15.0 - eqTime/60.0
|
||||
sunsetTime := solarNoon + hourAngle/15.0 - lon/15.0 - eqTime/60.0
|
||||
|
||||
sunrise := timeOfDayToTime(sunriseTime, year, month, day, time.UTC).In(loc)
|
||||
sunset := timeOfDayToTime(sunsetTime, year, month, day, time.UTC).In(loc)
|
||||
|
||||
return SunTimes{
|
||||
Sunrise: sunrise,
|
||||
Sunset: sunset,
|
||||
}
|
||||
}
|
||||
|
||||
func timeOfDayToTime(hours float64, year int, month time.Month, day int, loc *time.Location) time.Time {
|
||||
h := int(hours)
|
||||
m := int((hours - float64(h)) * 60)
|
||||
s := int(((hours-float64(h))*60 - float64(m)) * 60)
|
||||
|
||||
if h < 0 {
|
||||
h += 24
|
||||
day--
|
||||
}
|
||||
if h >= 24 {
|
||||
h -= 24
|
||||
day++
|
||||
}
|
||||
|
||||
return time.Date(year, month, day, h, m, s, 0, loc)
|
||||
}
|
||||
378
backend/internal/server/wayland/suncalc_test.go
Normal file
378
backend/internal/server/wayland/suncalc_test.go
Normal file
@@ -0,0 +1,378 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func calculateTemperature(config Config, now time.Time) int {
|
||||
if !config.Enabled {
|
||||
return config.HighTemp
|
||||
}
|
||||
|
||||
var sunrise, sunset time.Time
|
||||
|
||||
if config.ManualSunrise != nil && config.ManualSunset != nil {
|
||||
year, month, day := now.Date()
|
||||
loc := now.Location()
|
||||
|
||||
sunrise = time.Date(year, month, day,
|
||||
config.ManualSunrise.Hour(),
|
||||
config.ManualSunrise.Minute(),
|
||||
config.ManualSunrise.Second(), 0, loc)
|
||||
sunset = time.Date(year, month, day,
|
||||
config.ManualSunset.Hour(),
|
||||
config.ManualSunset.Minute(),
|
||||
config.ManualSunset.Second(), 0, loc)
|
||||
|
||||
if sunset.Before(sunrise) {
|
||||
sunset = sunset.Add(24 * time.Hour)
|
||||
}
|
||||
} else if config.UseIPLocation {
|
||||
lat, lon, err := FetchIPLocation()
|
||||
if err != nil {
|
||||
return config.HighTemp
|
||||
}
|
||||
times := CalculateSunTimes(*lat, *lon, now)
|
||||
sunrise = times.Sunrise
|
||||
sunset = times.Sunset
|
||||
} else if config.Latitude != nil && config.Longitude != nil {
|
||||
times := CalculateSunTimes(*config.Latitude, *config.Longitude, now)
|
||||
sunrise = times.Sunrise
|
||||
sunset = times.Sunset
|
||||
} else {
|
||||
return config.HighTemp
|
||||
}
|
||||
|
||||
if now.Before(sunrise) || now.After(sunset) {
|
||||
return config.LowTemp
|
||||
}
|
||||
return config.HighTemp
|
||||
}
|
||||
|
||||
func calculateNextTransition(config Config, now time.Time) time.Time {
|
||||
if !config.Enabled {
|
||||
return now.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
var sunrise, sunset time.Time
|
||||
|
||||
if config.ManualSunrise != nil && config.ManualSunset != nil {
|
||||
year, month, day := now.Date()
|
||||
loc := now.Location()
|
||||
|
||||
sunrise = time.Date(year, month, day,
|
||||
config.ManualSunrise.Hour(),
|
||||
config.ManualSunrise.Minute(),
|
||||
config.ManualSunrise.Second(), 0, loc)
|
||||
sunset = time.Date(year, month, day,
|
||||
config.ManualSunset.Hour(),
|
||||
config.ManualSunset.Minute(),
|
||||
config.ManualSunset.Second(), 0, loc)
|
||||
|
||||
if sunset.Before(sunrise) {
|
||||
sunset = sunset.Add(24 * time.Hour)
|
||||
}
|
||||
} else if config.UseIPLocation {
|
||||
lat, lon, err := FetchIPLocation()
|
||||
if err != nil {
|
||||
return now.Add(24 * time.Hour)
|
||||
}
|
||||
times := CalculateSunTimes(*lat, *lon, now)
|
||||
sunrise = times.Sunrise
|
||||
sunset = times.Sunset
|
||||
} else if config.Latitude != nil && config.Longitude != nil {
|
||||
times := CalculateSunTimes(*config.Latitude, *config.Longitude, now)
|
||||
sunrise = times.Sunrise
|
||||
sunset = times.Sunset
|
||||
} else {
|
||||
return now.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
if now.Before(sunrise) {
|
||||
return sunrise
|
||||
}
|
||||
if now.Before(sunset) {
|
||||
return sunset
|
||||
}
|
||||
|
||||
if config.ManualSunrise != nil && config.ManualSunset != nil {
|
||||
year, month, day := now.Add(24 * time.Hour).Date()
|
||||
loc := now.Location()
|
||||
nextSunrise := time.Date(year, month, day,
|
||||
config.ManualSunrise.Hour(),
|
||||
config.ManualSunrise.Minute(),
|
||||
config.ManualSunrise.Second(), 0, loc)
|
||||
return nextSunrise
|
||||
}
|
||||
|
||||
if config.UseIPLocation {
|
||||
lat, lon, err := FetchIPLocation()
|
||||
if err != nil {
|
||||
return now.Add(24 * time.Hour)
|
||||
}
|
||||
nextDayTimes := CalculateSunTimes(*lat, *lon, now.Add(24*time.Hour))
|
||||
return nextDayTimes.Sunrise
|
||||
}
|
||||
|
||||
if config.Latitude != nil && config.Longitude != nil {
|
||||
nextDayTimes := CalculateSunTimes(*config.Latitude, *config.Longitude, now.Add(24*time.Hour))
|
||||
return nextDayTimes.Sunrise
|
||||
}
|
||||
|
||||
return now.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
func TestCalculateSunTimes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lat float64
|
||||
lon float64
|
||||
date time.Time
|
||||
checkFunc func(*testing.T, SunTimes)
|
||||
}{
|
||||
{
|
||||
name: "new_york_summer",
|
||||
lat: 40.7128,
|
||||
lon: -74.0060,
|
||||
date: time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local),
|
||||
checkFunc: func(t *testing.T, times SunTimes) {
|
||||
if times.Sunrise.Hour() < 4 || times.Sunrise.Hour() > 6 {
|
||||
t.Logf("sunrise: %v", times.Sunrise)
|
||||
}
|
||||
if times.Sunset.Hour() < 19 || times.Sunset.Hour() > 21 {
|
||||
t.Logf("sunset: %v", times.Sunset)
|
||||
}
|
||||
if !times.Sunset.After(times.Sunrise) {
|
||||
t.Error("sunset should be after sunrise")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "london_winter",
|
||||
lat: 51.5074,
|
||||
lon: -0.1278,
|
||||
date: time.Date(2024, 12, 21, 12, 0, 0, 0, time.UTC),
|
||||
checkFunc: func(t *testing.T, times SunTimes) {
|
||||
if times.Sunrise.Hour() < 7 || times.Sunrise.Hour() > 9 {
|
||||
t.Errorf("unexpected sunrise hour: %d", times.Sunrise.Hour())
|
||||
}
|
||||
if times.Sunset.Hour() < 15 || times.Sunset.Hour() > 17 {
|
||||
t.Errorf("unexpected sunset hour: %d", times.Sunset.Hour())
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "equator_equinox",
|
||||
lat: 0.0,
|
||||
lon: 0.0,
|
||||
date: time.Date(2024, 3, 20, 12, 0, 0, 0, time.UTC),
|
||||
checkFunc: func(t *testing.T, times SunTimes) {
|
||||
if times.Sunrise.Hour() < 5 || times.Sunrise.Hour() > 7 {
|
||||
t.Errorf("unexpected sunrise hour: %d", times.Sunrise.Hour())
|
||||
}
|
||||
if times.Sunset.Hour() < 17 || times.Sunset.Hour() > 19 {
|
||||
t.Errorf("unexpected sunset hour: %d", times.Sunset.Hour())
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
times := CalculateSunTimes(tt.lat, tt.lon, tt.date)
|
||||
tt.checkFunc(t, times)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateTemperature(t *testing.T) {
|
||||
lat := 40.7128
|
||||
lon := -74.0060
|
||||
date := time.Date(2024, 6, 21, 0, 0, 0, 0, time.Local)
|
||||
|
||||
config := Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: &lat,
|
||||
Longitude: &lon,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
times := CalculateSunTimes(lat, lon, date)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
timeFunc func() time.Time
|
||||
wantTemp int
|
||||
}{
|
||||
{
|
||||
name: "midnight",
|
||||
timeFunc: func() time.Time { return times.Sunrise.Add(-4 * time.Hour) },
|
||||
wantTemp: 4000,
|
||||
},
|
||||
{
|
||||
name: "sunrise",
|
||||
timeFunc: func() time.Time { return times.Sunrise },
|
||||
wantTemp: 6500,
|
||||
},
|
||||
{
|
||||
name: "noon",
|
||||
timeFunc: func() time.Time { return times.Sunrise.Add(6 * time.Hour) },
|
||||
wantTemp: 6500,
|
||||
},
|
||||
{
|
||||
name: "after_sunset_transition",
|
||||
timeFunc: func() time.Time { return times.Sunset.Add(2 * time.Hour) },
|
||||
wantTemp: 4000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
temp := calculateTemperature(config, tt.timeFunc())
|
||||
|
||||
if math.Abs(float64(temp-tt.wantTemp)) > 500 {
|
||||
t.Errorf("temperature = %d, want approximately %d", temp, tt.wantTemp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateTemperatureManualTimes(t *testing.T) {
|
||||
sunrise := time.Date(0, 1, 1, 6, 30, 0, 0, time.Local)
|
||||
sunset := time.Date(0, 1, 1, 18, 30, 0, 0, time.Local)
|
||||
|
||||
config := Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
ManualSunrise: &sunrise,
|
||||
ManualSunset: &sunset,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
time time.Time
|
||||
want int
|
||||
}{
|
||||
{"before_sunrise", time.Date(2024, 1, 1, 3, 0, 0, 0, time.Local), 4000},
|
||||
{"at_sunrise", time.Date(2024, 1, 1, 6, 30, 0, 0, time.Local), 6500},
|
||||
{"midday", time.Date(2024, 1, 1, 12, 0, 0, 0, time.Local), 6500},
|
||||
{"at_sunset", time.Date(2024, 1, 1, 18, 30, 0, 0, time.Local), 6500},
|
||||
{"after_sunset", time.Date(2024, 1, 1, 22, 0, 0, 0, time.Local), 4000},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
temp := calculateTemperature(config, tt.time)
|
||||
if math.Abs(float64(temp-tt.want)) > 500 {
|
||||
t.Errorf("temperature = %d, want approximately %d", temp, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateTemperatureDisabled(t *testing.T) {
|
||||
lat := 40.7128
|
||||
lon := -74.0060
|
||||
|
||||
config := Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: &lat,
|
||||
Longitude: &lon,
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
temp := calculateTemperature(config, time.Now())
|
||||
if temp != 6500 {
|
||||
t.Errorf("disabled should return high temp, got %d", temp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateNextTransition(t *testing.T) {
|
||||
lat := 40.7128
|
||||
lon := -74.0060
|
||||
date := time.Date(2024, 6, 21, 0, 0, 0, 0, time.Local)
|
||||
|
||||
config := Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: &lat,
|
||||
Longitude: &lon,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
times := CalculateSunTimes(lat, lon, date)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
now time.Time
|
||||
checkFunc func(*testing.T, time.Time)
|
||||
}{
|
||||
{
|
||||
name: "before_sunrise",
|
||||
now: times.Sunrise.Add(-2 * time.Hour),
|
||||
checkFunc: func(t *testing.T, next time.Time) {
|
||||
if !next.Equal(times.Sunrise) && !next.After(times.Sunrise.Add(-1*time.Minute)) {
|
||||
t.Error("next transition should be at or near sunrise")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "after_sunrise",
|
||||
now: times.Sunrise.Add(2 * time.Hour),
|
||||
checkFunc: func(t *testing.T, next time.Time) {
|
||||
if !next.After(times.Sunrise) {
|
||||
t.Error("next transition should be after sunrise")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
next := calculateNextTransition(config, tt.now)
|
||||
tt.checkFunc(t, next)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeOfDayToTime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hours float64
|
||||
expected time.Time
|
||||
}{
|
||||
{
|
||||
name: "noon",
|
||||
hours: 12.0,
|
||||
expected: time.Date(2024, 6, 21, 12, 0, 0, 0, time.Local),
|
||||
},
|
||||
{
|
||||
name: "half_past",
|
||||
hours: 12.5,
|
||||
expected: time.Date(2024, 6, 21, 12, 30, 0, 0, time.Local),
|
||||
},
|
||||
{
|
||||
name: "early_morning",
|
||||
hours: 6.25,
|
||||
expected: time.Date(2024, 6, 21, 6, 15, 0, 0, time.Local),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := timeOfDayToTime(tt.hours, 2024, 6, 21, time.Local)
|
||||
|
||||
if result.Hour() != tt.expected.Hour() {
|
||||
t.Errorf("hour = %d, want %d", result.Hour(), tt.expected.Hour())
|
||||
}
|
||||
if result.Minute() != tt.expected.Minute() {
|
||||
t.Errorf("minute = %d, want %d", result.Minute(), tt.expected.Minute())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
194
backend/internal/server/wayland/types.go
Normal file
194
backend/internal/server/wayland/types.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/backend/internal/errdefs"
|
||||
"github.com/godbus/dbus/v5"
|
||||
wlclient "github.com/yaslama/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Outputs []string
|
||||
LowTemp int
|
||||
HighTemp int
|
||||
Latitude *float64
|
||||
Longitude *float64
|
||||
UseIPLocation bool
|
||||
ManualSunrise *time.Time
|
||||
ManualSunset *time.Time
|
||||
ManualDuration *time.Duration
|
||||
Gamma float64
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Config Config `json:"config"`
|
||||
CurrentTemp int `json:"currentTemp"`
|
||||
NextTransition time.Time `json:"nextTransition"`
|
||||
SunriseTime time.Time `json:"sunriseTime"`
|
||||
SunsetTime time.Time `json:"sunsetTime"`
|
||||
IsDay bool `json:"isDay"`
|
||||
}
|
||||
|
||||
type cmd struct {
|
||||
fn func()
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
config Config
|
||||
configMutex sync.RWMutex
|
||||
state *State
|
||||
stateMutex sync.RWMutex
|
||||
|
||||
display *wlclient.Display
|
||||
registry *wlclient.Registry
|
||||
gammaControl interface{}
|
||||
availableOutputs []*wlclient.Output
|
||||
outputRegNames map[uint32]uint32
|
||||
outputs map[uint32]*outputState
|
||||
outputsMutex sync.RWMutex
|
||||
controlsInitialized bool
|
||||
|
||||
cmdq chan cmd
|
||||
alive bool
|
||||
|
||||
stopChan chan struct{}
|
||||
updateTrigger chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
currentTemp int
|
||||
targetTemp int
|
||||
transitionMutex sync.RWMutex
|
||||
transitionChan chan int
|
||||
|
||||
cachedIPLat *float64
|
||||
cachedIPLon *float64
|
||||
locationMutex sync.RWMutex
|
||||
|
||||
subscribers map[string]chan State
|
||||
subMutex sync.RWMutex
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotified *State
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
dbusSignal chan *dbus.Signal
|
||||
}
|
||||
|
||||
type outputState struct {
|
||||
id uint32
|
||||
name string
|
||||
registryName uint32
|
||||
output *wlclient.Output
|
||||
gammaControl interface{}
|
||||
rampSize uint32
|
||||
failed bool
|
||||
isVirtual bool
|
||||
retryCount int
|
||||
lastFailTime time.Time
|
||||
}
|
||||
|
||||
type SunTimes struct {
|
||||
Sunrise time.Time
|
||||
Sunset time.Time
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Outputs: []string{},
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Gamma: 1.0,
|
||||
Enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.LowTemp < 1000 || c.LowTemp > 10000 {
|
||||
return errdefs.ErrInvalidTemperature
|
||||
}
|
||||
if c.HighTemp < 1000 || c.HighTemp > 10000 {
|
||||
return errdefs.ErrInvalidTemperature
|
||||
}
|
||||
if c.LowTemp > c.HighTemp {
|
||||
return errdefs.ErrInvalidTemperature
|
||||
}
|
||||
if c.Gamma <= 0 || c.Gamma > 10 {
|
||||
return errdefs.ErrInvalidGamma
|
||||
}
|
||||
if c.Latitude != nil && (math.Abs(*c.Latitude) > 90) {
|
||||
return errdefs.ErrInvalidLocation
|
||||
}
|
||||
if c.Longitude != nil && (math.Abs(*c.Longitude) > 180) {
|
||||
return errdefs.ErrInvalidLocation
|
||||
}
|
||||
if (c.Latitude != nil) != (c.Longitude != nil) {
|
||||
return errdefs.ErrInvalidLocation
|
||||
}
|
||||
if (c.ManualSunrise != nil) != (c.ManualSunset != nil) {
|
||||
return errdefs.ErrInvalidManualTimes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
if m.state == nil {
|
||||
return State{}
|
||||
}
|
||||
stateCopy := *m.state
|
||||
return stateCopy
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan State {
|
||||
ch := make(chan State, 64)
|
||||
m.subMutex.Lock()
|
||||
m.subscribers[id] = ch
|
||||
m.subMutex.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subMutex.Lock()
|
||||
if ch, ok := m.subscribers[id]; ok {
|
||||
close(ch)
|
||||
delete(m.subscribers, id)
|
||||
}
|
||||
m.subMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func stateChanged(old, new *State) bool {
|
||||
if old == nil || new == nil {
|
||||
return true
|
||||
}
|
||||
if old.CurrentTemp != new.CurrentTemp {
|
||||
return true
|
||||
}
|
||||
if old.IsDay != new.IsDay {
|
||||
return true
|
||||
}
|
||||
if !old.NextTransition.Equal(new.NextTransition) {
|
||||
return true
|
||||
}
|
||||
if !old.SunriseTime.Equal(new.SunriseTime) {
|
||||
return true
|
||||
}
|
||||
if !old.SunsetTime.Equal(new.SunsetTime) {
|
||||
return true
|
||||
}
|
||||
if old.Config.Enabled != new.Config.Enabled {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
330
backend/internal/server/wayland/types_test.go
Normal file
330
backend/internal/server/wayland/types_test.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package wayland
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfigValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid_default",
|
||||
config: DefaultConfig(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid_with_location",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: floatPtr(40.7128),
|
||||
Longitude: floatPtr(-74.0060),
|
||||
Gamma: 1.0,
|
||||
Enabled: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid_manual_times",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
ManualSunrise: timePtr(time.Date(0, 1, 1, 6, 30, 0, 0, time.Local)),
|
||||
ManualSunset: timePtr(time.Date(0, 1, 1, 18, 30, 0, 0, time.Local)),
|
||||
Gamma: 1.0,
|
||||
Enabled: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid_low_temp_too_low",
|
||||
config: Config{
|
||||
LowTemp: 500,
|
||||
HighTemp: 6500,
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_low_temp_too_high",
|
||||
config: Config{
|
||||
LowTemp: 15000,
|
||||
HighTemp: 20000,
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_high_temp_too_low",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 500,
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid_temps_equal",
|
||||
config: Config{
|
||||
LowTemp: 5000,
|
||||
HighTemp: 5000,
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid_temps_reversed",
|
||||
config: Config{
|
||||
LowTemp: 6500,
|
||||
HighTemp: 4000,
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_gamma_zero",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Gamma: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_gamma_negative",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Gamma: -1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_gamma_too_high",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Gamma: 15.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_latitude_too_high",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: floatPtr(100),
|
||||
Longitude: floatPtr(0),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_latitude_too_low",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: floatPtr(-100),
|
||||
Longitude: floatPtr(0),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_longitude_too_high",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: floatPtr(40),
|
||||
Longitude: floatPtr(200),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_longitude_too_low",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: floatPtr(40),
|
||||
Longitude: floatPtr(-200),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_latitude_without_longitude",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Latitude: floatPtr(40),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_longitude_without_latitude",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Longitude: floatPtr(-74),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_sunrise_without_sunset",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
ManualSunrise: timePtr(time.Date(0, 1, 1, 6, 30, 0, 0, time.Local)),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_sunset_without_sunrise",
|
||||
config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
ManualSunset: timePtr(time.Date(0, 1, 1, 18, 30, 0, 0, time.Local)),
|
||||
Gamma: 1.0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.config.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
config := DefaultConfig()
|
||||
|
||||
if config.LowTemp != 4000 {
|
||||
t.Errorf("default low temp = %d, want 4000", config.LowTemp)
|
||||
}
|
||||
if config.HighTemp != 6500 {
|
||||
t.Errorf("default high temp = %d, want 6500", config.HighTemp)
|
||||
}
|
||||
if config.Gamma != 1.0 {
|
||||
t.Errorf("default gamma = %f, want 1.0", config.Gamma)
|
||||
}
|
||||
if config.Enabled {
|
||||
t.Error("default should be disabled")
|
||||
}
|
||||
if config.Latitude != nil {
|
||||
t.Error("default should not have latitude")
|
||||
}
|
||||
if config.Longitude != nil {
|
||||
t.Error("default should not have longitude")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateChanged(t *testing.T) {
|
||||
baseState := &State{
|
||||
CurrentTemp: 5000,
|
||||
NextTransition: time.Now(),
|
||||
SunriseTime: time.Now().Add(6 * time.Hour),
|
||||
SunsetTime: time.Now().Add(18 * time.Hour),
|
||||
IsDay: true,
|
||||
Config: DefaultConfig(),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
old *State
|
||||
new *State
|
||||
wantChanged bool
|
||||
}{
|
||||
{
|
||||
name: "nil_old",
|
||||
old: nil,
|
||||
new: baseState,
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "nil_new",
|
||||
old: baseState,
|
||||
new: nil,
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "same_state",
|
||||
old: baseState,
|
||||
new: baseState,
|
||||
wantChanged: false,
|
||||
},
|
||||
{
|
||||
name: "temp_changed",
|
||||
old: baseState,
|
||||
new: &State{
|
||||
CurrentTemp: 6000,
|
||||
NextTransition: baseState.NextTransition,
|
||||
SunriseTime: baseState.SunriseTime,
|
||||
SunsetTime: baseState.SunsetTime,
|
||||
IsDay: baseState.IsDay,
|
||||
Config: baseState.Config,
|
||||
},
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "is_day_changed",
|
||||
old: baseState,
|
||||
new: &State{
|
||||
CurrentTemp: baseState.CurrentTemp,
|
||||
NextTransition: baseState.NextTransition,
|
||||
SunriseTime: baseState.SunriseTime,
|
||||
SunsetTime: baseState.SunsetTime,
|
||||
IsDay: false,
|
||||
Config: baseState.Config,
|
||||
},
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "enabled_changed",
|
||||
old: baseState,
|
||||
new: &State{
|
||||
CurrentTemp: baseState.CurrentTemp,
|
||||
NextTransition: baseState.NextTransition,
|
||||
SunriseTime: baseState.SunriseTime,
|
||||
SunsetTime: baseState.SunsetTime,
|
||||
IsDay: baseState.IsDay,
|
||||
Config: Config{
|
||||
LowTemp: 4000,
|
||||
HighTemp: 6500,
|
||||
Gamma: 1.0,
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
wantChanged: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changed := stateChanged(tt.old, tt.new)
|
||||
if changed != tt.wantChanged {
|
||||
t.Errorf("stateChanged() = %v, want %v", changed, tt.wantChanged)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func floatPtr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func timePtr(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
Reference in New Issue
Block a user