refactor: remove temporary files

doc: add readme
This commit is contained in:
Simon Vieille 2025-11-12 18:24:46 +01:00
commit 6a9b5db10d
Signed by: deblan
GPG key ID: 579388D585F70417
8 changed files with 213 additions and 65 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/ha-rgb-screen

36
README.md Normal file
View file

@ -0,0 +1,36 @@
# RGB Screen Sync for Home Assistant
## 🌈 Overview
This project synchronizes your RGB lamp (connected via Home Assistant) with the average color displayed on your screen in real time. The goal is to create an immersive ambient lighting experience that reacts dynamically to your screens content — perfect for movies, gaming, or music visualization.
## 🧩 How It Works
- The script captures a screenshot of your screen
- It calculates the average RGB color
- It sends a color update command to your Home Assistant instance via its REST API
- Your RGB lamp updates its color to match the screen
## Home Assistant configuration
### Create an automation
![](https://upload.deblan.org/u/2025-11/6914c190.png)
![](https://upload.deblan.org/u/2025-11/6914c1ef.png)
### Set a trigger and copy the URL
![](https://upload.deblan.org/u/2025-11/6914c254.png)
### Set an action
![](https://upload.deblan.org/u/2025-11/6914c2a3.png)
## 🧪 Example
```
ha-rgb-screen -url http://homeassistant.local:8123/api/webhook/xxxxx -screen 0 -d 500
```
Runs the RGB Screen Sync tool and connects it to your Home Assistant instance via a webhook. The `-url` parameter specifies the webhook endpoint used to send color updates. The `-screen 0` option selects the first display (useful if multiple monitors are connected), and the `-d 500` option sets the delay between color updates to 500 milliseconds — meaning the lamps color will refresh twice per second to match the average color of the chosen screen.

39
cmd/main.go Normal file
View file

@ -0,0 +1,39 @@
package main
import (
"log"
"os"
"time"
"gitnet.fr/deblan/ha-rgb-screen/internal/config"
"gitnet.fr/deblan/ha-rgb-screen/internal/ha"
"gitnet.fr/deblan/ha-rgb-screen/internal/img"
"gitnet.fr/deblan/ha-rgb-screen/internal/screen"
)
func main() {
params := config.GetConfig()
if !params.IsValid() {
params.Usage()
os.Exit(1)
}
s := screen.NewScreen(params)
client := ha.NewClient(params)
for {
capture, err := s.Capture()
if err == nil {
if err := client.Update(img.GetRgbAverage(capture)); err != nil {
log.Println("Error: %s", err.Error())
}
} else {
log.Fatalf("Fatal error: %s", err.Error())
}
time.Sleep(time.Duration(params.Delay) * time.Millisecond)
}
}

36
internal/config/config.go Normal file
View file

@ -0,0 +1,36 @@
package config
import "flag"
type Config struct {
Url string
Delay int
Screen int
Verbose bool
Usage func()
}
func (c *Config) IsValid() bool {
if c.Url == "" {
return false
}
return true
}
func GetConfig() *Config {
url := flag.String("url", "", "Webhook URL")
delay := flag.Int("delay", 500, "Delay in ms")
screen := flag.Int("screen", 0, "Screen index")
verbose := flag.Bool("v", false, "Verbose mode")
flag.Parse()
return &Config{
Url: *url,
Delay: *delay,
Screen: *screen,
Verbose: *verbose,
Usage: flag.Usage,
}
}

47
internal/ha/client.go Normal file
View file

@ -0,0 +1,47 @@
package ha
import (
"bytes"
"fmt"
"log"
"net/http"
"gitnet.fr/deblan/ha-rgb-screen/internal/config"
"gitnet.fr/deblan/ha-rgb-screen/internal/img"
)
type Client struct {
params *config.Config
client *http.Client
}
func NewClient(params *config.Config) *Client {
return &Client{
params: params,
client: new(http.Client),
}
}
func (c *Client) Update(rgb img.RGB) error {
data := fmt.Sprintf(`{"rgb": [%.0f, %.0f, %.0f]}`, rgb.R, rgb.G, rgb.B)
if c.params.Verbose {
log.Println(data)
}
req, err := http.NewRequest(
"POST",
c.params.Url,
bytes.NewBuffer([]byte(data)),
)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
_, err = c.client.Do(req)
return err
}

32
internal/img/image.go Normal file
View file

@ -0,0 +1,32 @@
package img
import "image"
type RGB struct {
R float64
G float64
B float64
}
func GetRgbAverage(i *image.RGBA) RGB {
bounds := i.Bounds()
var rSum, gSum, bSum uint64
var count uint64
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, _ := i.At(x, y).RGBA()
rSum += uint64(r >> 8)
gSum += uint64(g >> 8)
bSum += uint64(b >> 8)
count++
}
}
return RGB{
R: float64(rSum) / float64(count),
G: float64(gSum) / float64(count),
B: float64(bSum) / float64(count),
}
}

22
internal/screen/screen.go Normal file
View file

@ -0,0 +1,22 @@
package screen
import (
"image"
s "github.com/kbinani/screenshot"
"gitnet.fr/deblan/ha-rgb-screen/internal/config"
)
type Screen struct {
displayBounds image.Rectangle
}
func NewScreen(params *config.Config) *Screen {
return &Screen{
displayBounds: s.GetDisplayBounds(params.Screen),
}
}
func (sc *Screen) Capture() (*image.RGBA, error) {
return s.CaptureRect(sc.displayBounds)
}

65
main.go
View file

@ -1,65 +0,0 @@
package main
import (
"bytes"
"flag"
"fmt"
_ "image/jpeg" // pour pouvoir lire les fichiers .jpg
_ "image/png" // pour pouvoir lire les fichiers .png
"log"
"net/http"
"time"
"github.com/kbinani/screenshot"
)
func main() {
webhook := flag.String("webhook", "", "Webhook URL")
flag.Parse()
if *webhook == "" {
log.Fatal("webhook is required")
}
displayBounds := screenshot.GetDisplayBounds(0)
client := &http.Client{}
for {
if img, err := screenshot.CaptureRect(displayBounds); err == nil {
bounds := img.Bounds()
var rSum, gSum, bSum uint64
var count uint64
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, _ := img.At(x, y).RGBA()
rSum += uint64(r >> 8)
gSum += uint64(g >> 8)
bSum += uint64(b >> 8)
count++
}
}
rAvg := float64(rSum) / float64(count)
gAvg := float64(gSum) / float64(count)
bAvg := float64(bSum) / float64(count)
data := fmt.Sprintf(`{"rgb": [%.0f, %.0f, %.0f]}`, rAvg, gAvg, bAvg)
req, _ := http.NewRequest(
"POST",
*webhook,
bytes.NewBuffer([]byte(data)),
)
log.Println(data)
req.Header.Set("Content-Type", "application/json")
client.Do(req)
}
time.Sleep(1 * time.Second)
}
}