From 12520dde9ef617558ce4113de748bd634b8532b3 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Fri, 12 Apr 2024 08:46:01 -0400
Subject: [PATCH 01/11] Create GameSync2024v2.py
---
GameSync2024v2.py | 176 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 176 insertions(+)
create mode 100644 GameSync2024v2.py
diff --git a/GameSync2024v2.py b/GameSync2024v2.py
new file mode 100644
index 0000000..c0cf73a
--- /dev/null
+++ b/GameSync2024v2.py
@@ -0,0 +1,176 @@
+import base64
+import socket
+import time
+from enum import Enum
+
+from PIL import ImageGrab
+from pydantic import BaseModel, Field, validator
+
+# Configuration
+# DEVICE_IP: list[str] = ["10.1.1.54", "10.1.1.43"] # Example IPs for two devices
+DEVICE_IP: list[str] = ["192.168.0.108", "192.168.0.248"]
+UDP_PORT: int = 4003 # Default UDP port
+NUM_COLORS: int = 20 # Number of color samples per device
+
+
+class PowerState(Enum):
+ OFF = 0
+ ON = 1
+
+
+class Color(BaseModel):
+ r: int = Field(..., ge=0, le=255)
+ g: int = Field(..., ge=0, le=255)
+ b: int = Field(..., ge=0, le=255)
+
+
+class Command(BaseModel):
+ msg: dict
+
+
+class PowerCommand(Command):
+ msg: dict = {"cmd": "turn", "data": {"value": PowerState.OFF.value}}
+
+ @validator("msg", pre=True)
+ def set_power_state(cls, value, values):
+ value["data"]["value"] = values.get("power_state", PowerState.OFF).value
+ return value
+
+
+class BrightnessCommand(Command):
+ msg: dict = {"cmd": "brightness", "data": {"value": 100}}
+
+ @validator("msg", pre=True)
+ def set_brightness(cls, value, values):
+ value["data"]["value"] = values.get("brightness", 100)
+ return value
+
+
+class ColorCommand(Command):
+ msg: dict = {
+ "cmd": "colorwc",
+ "data": {"color": {"r": 255, "g": 255, "b": 255}, "colorTemInKelvin": 0},
+ }
+
+ @validator("msg", pre=True)
+ def set_color(cls, value, values):
+ color = values.get("color", Color(r=255, g=255, b=255))
+ value["data"]["color"] = color.dict()
+ return value
+
+
+class SegmentCommand(Command):
+ msg: dict = {"cmd": "razer", "data": {"pt": "uwABsQEK"}}
+
+ @validator("msg", pre=True)
+ def set_segment(cls, value, values):
+ value["data"]["pt"] = values.get("segment", "uwABsQEK")
+ return value
+
+
+class FormatAndSendColorsCommand(Command):
+ msg: dict = {"cmd": "razer", "data": {"pt": ""}}
+
+ @validator("msg", pre=True)
+ def set_colors(cls, value, values):
+ colors = values.get("colors", [])
+ byteArray: list[int] = [187, 0, 32, 176, 1, NUM_COLORS] + [
+ c for color in colors for c in color
+ ]
+ checksum: int = sum(byteArray) % 256
+ byteArray.append(checksum)
+ encoded_data: str = base64.b64encode(bytes(byteArray)).decode()
+ value["data"]["pt"] = encoded_data
+ return value
+
+
+def send_command(udp_ip: str, command: Command) -> None:
+ """Send command data to a specific IP using UDP."""
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
+ sock.sendto(command.json().encode(), (udp_ip, UDP_PORT))
+ print(f"Command sent to {udp_ip}")
+
+
+def capture_screen_and_process_colors(
+ num_devices: int,
+) -> list[list[tuple[int, int, int]]]:
+ """Capture screen, compute average colors for specified points, and return them."""
+ screen_image = ImageGrab.grab() # Capture the entire screen
+ screen_width, screen_height = screen_image.size
+
+ # Define sample positions on the screen
+ vertical_positions: list[int] = [
+ int(screen_height * i / NUM_COLORS) for i in range(NUM_COLORS)
+ ]
+
+ # Calculate horizontal sections for each device
+ horizontal_sections: list[int] = [
+ int(screen_width * (i + 1) / (num_devices + 1)) for i in range(num_devices)
+ ]
+
+ device_colors: list[list[tuple[int, int, int]]] = []
+ for x in horizontal_sections:
+ section_colors: list[tuple[int, int, int]] = [
+ screen_image.getpixel((x, y)) for y in vertical_positions
+ ]
+ device_colors.append(section_colors)
+
+ return device_colors
+
+
+def game_loop() -> None:
+ """Main game loop to handle screen capture, color processing, and device control."""
+ try:
+ for ip in DEVICE_IP:
+ send_command(
+ ip, PowerCommand(power_state=PowerState.ON)
+ ) # Turn on the devices
+ send_command(
+ ip, BrightnessCommand(brightness=100)
+ ) # Set brightness to 100%
+ send_command(ip, SegmentCommand(segment="uwABsQEK")) # Initialize segments
+ time.sleep(2)
+
+ while True:
+ color_data: list[list[tuple[int, int, int]]] = (
+ capture_screen_and_process_colors(len(DEVICE_IP))
+ )
+ for ip, colors in zip(DEVICE_IP, color_data):
+ send_command(ip, FormatAndSendColorsCommand(colors=colors))
+ time.sleep(1) # Update interval
+
+ except KeyboardInterrupt:
+ print("Operation stopped by user.")
+ finally:
+ for ip in DEVICE_IP:
+ send_command(
+ ip, ColorCommand(color=Color(r=255, g=165, b=0))
+ ) # Set light sunset color (orange)
+ print("Setting light to orange...")
+ time.sleep(1)
+ for ip in DEVICE_IP:
+ print("Turning off devices...")
+ send_command(ip, SegmentCommand(segment="uwABsQAL")) # Terminate segments
+ send_command(
+ ip, PowerCommand(power_state=PowerState.OFF)
+ ) # Turn off the devices
+
+
+def main() -> None:
+ """Main function to handle the overall operation."""
+ print("Press Ctrl+C to quit.")
+ game_loop()
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except Exception as e:
+ print(f"An error occurred: {e}")
+ finally:
+ for ip in DEVICE_IP:
+ print("Turning off devices...")
+ send_command(ip, SegmentCommand(segment="uwABsQAL")) # Terminate segments
+ send_command(
+ ip, PowerCommand(power_state=PowerState.OFF)
+ ) # Turn off the devices
From dfdfdd0df5094ecb836c3ad75013ccfc906009d1 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sat, 13 Apr 2024 09:49:01 -0400
Subject: [PATCH 02/11] Create ScreenSync.py
---
ScreenSync.py | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 298 insertions(+)
create mode 100644 ScreenSync.py
diff --git a/ScreenSync.py b/ScreenSync.py
new file mode 100644
index 0000000..9f3a754
--- /dev/null
+++ b/ScreenSync.py
@@ -0,0 +1,298 @@
+import base64
+import signal
+import socket
+import time
+from enum import Enum
+from typing import Union
+
+import numpy as np
+from PIL import ImageEnhance, ImageGrab
+from pydantic import BaseModel, Field
+
+UDP_PORT = 4003
+MAX_LED_COLOR_GRADIENT = 10
+# MAX_LED_COLOR_GRADIENT = 4
+
+
+class PowerState(Enum):
+ OFF = 0
+ ON = 1
+
+
+# Pydantic Models
+class Color(BaseModel):
+ r: int = Field(..., ge=0, le=255)
+ g: int = Field(..., ge=0, le=255)
+ b: int = Field(..., ge=0, le=255)
+
+ @property
+ def rgb(self):
+ return (self.r, self.g, self.b)
+
+
+class PowerData(BaseModel):
+ value: PowerState
+
+
+class BrightnessData(BaseModel):
+ value: int
+
+
+class ColorData(BaseModel):
+ color: Color
+ colorTemInKelvin: int = 0
+
+
+class SegmentData(BaseModel):
+ pt: str
+
+
+class Command(BaseModel):
+ cmd: str
+ data: Union[PowerData, BrightnessData, ColorData, SegmentData]
+
+
+class Message(BaseModel):
+ msg: Command
+
+
+class PowerCommand(Command):
+ cmd: str = "turn"
+ data: PowerData
+
+
+class BrightnessCommand(Command):
+ cmd: str = "brightness"
+ data: BrightnessData
+
+
+class ColorCommand(Command):
+ cmd: str = "colorwc"
+ data: ColorData
+
+
+class SegmentCommand(Command):
+ cmd: str = "razer"
+ data: SegmentData
+
+
+class GoveeLightDevice:
+ def __init__(self, ip: str, name: str, screen_positions: list[tuple[int, int]]):
+ self.ip = ip
+ self.name = name
+ self.screen_positions = screen_positions
+
+ def _send_command(self, command: Command):
+ message = Message(msg=command).model_dump_json()
+
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
+ sock.sendto(message.encode(), (self.ip, UDP_PORT))
+ # print(f"Command sent to {ip}: {message}")
+
+ def power_on(self):
+ power_on_command = PowerCommand(data=PowerData(value=PowerState.ON))
+ self._send_command(power_on_command)
+
+ def power_off(self):
+ power_off_command = PowerCommand(data=PowerData(value=PowerState.OFF))
+ self._send_command(power_off_command)
+
+ def set_color(self, color: Color):
+ color_command = ColorCommand(data=ColorData(color=color))
+ self._send_command(color_command)
+
+ def set_brightness(self, brightness: int):
+ brightness_command = BrightnessCommand(data=BrightnessData(value=brightness))
+ self._send_command(brightness_command)
+
+ def initialize_segment(self):
+ """Initializes a segment to be set with set_segment_colors."""
+ segment_command = SegmentCommand(data=SegmentData(pt="uwABsQEK"))
+ self._send_command(segment_command)
+
+ def terminate_segment(self):
+ """Returns the color to the state before the segment was initialized."""
+ segment_command = SegmentCommand(data=SegmentData(pt="uwABsQAL"))
+ self._send_command(segment_command)
+
+ def set_segment_colors(self, list_of_colors: list[Color], gradient=True):
+ """Sets the colors of the segment to the given list of colors.
+ color list of size (min 2, max 10)
+
+ probably move "intrepolation" and "resolution" to some other place.
+
+ colors ordering goes from bottom to top for a lamp.
+ not sure about a strip.
+ but probably power source to the end of the strip.
+ """
+
+ assert (
+ 2 <= len(list_of_colors) <= MAX_LED_COLOR_GRADIENT
+ ), f"Color list must be between 1 and 10, got {len(list_of_colors)}"
+
+ segment_color_data = self._get_segment_color_data(list_of_colors, gradient)
+ segment_command = SegmentCommand(data=segment_color_data)
+ self._send_command(segment_command)
+
+ def _get_segment_color_data(
+ self, list_of_colors: list[Color], gradient=True
+ ) -> SegmentData:
+ gradient_flag = 1 if gradient else 0
+ byte_array = [187, 0, 32, 176, gradient_flag, len(list_of_colors)]
+
+ for color in list_of_colors:
+ byte_array.extend(color.rgb)
+
+ num2 = 0
+ for byte in byte_array:
+ num2 ^= byte
+ byte_array.append(num2)
+ final_send_value = base64.b64encode(bytes(byte_array)).decode()
+ return SegmentData(pt=final_send_value)
+
+
+def get_column_indices(column_number: int):
+ """
+ screen example for MAX_LED_COLOR_GRADIENT = 10
+
+ column numbers
+ 0 1 2 3 4 5 6 7 8 9
+ 0 x x x x x x x x x x
+ 1 x x x x x x x x x x
+ 2 x x x x x x x x x x
+ 3 x x x x x x x x x x
+ 4 x x x x x x x x x x
+ 5 x x x x x x x x x x
+ 6 x x x x x x x x x x
+ 7 x x x x x x x x x x
+ 8 x x x x x x x x x x
+ 9 x x x x x x x x x x
+
+ My device has index 0 at the bottom, so I need to reverse the row indices.
+ """
+ column_indices = [column_number] * MAX_LED_COLOR_GRADIENT
+ row_indices = list(range(MAX_LED_COLOR_GRADIENT))[::-1]
+ screen_indices = [column_indices, row_indices]
+ return screen_indices
+
+
+def example_usage():
+ default_color = Color(r=255, g=165, b=0)
+ # controller = LightController(["192.168.0.108", "192.168.0.248"])
+ left_column_screen_indices = [(0, i) for i in range(MAX_LED_COLOR_GRADIENT)]
+ left_column_device = GoveeLightDevice(
+ "192.168.0.248", "Govee Light Left", left_column_screen_indices
+ )
+
+ try:
+ # Example usage of the PowerCommand model
+ left_column_device.power_on()
+
+ # Example usage of the ColorCommand model
+ left_column_device.set_color(default_color)
+ time.sleep(1)
+
+ # Example usage of the BrightnessCommand model
+ left_column_device.set_brightness(50)
+ time.sleep(1)
+ left_column_device.set_brightness(100)
+ time.sleep(1)
+
+ # Example usage of the SegmentCommand model
+ left_column_device.initialize_segment()
+
+ left_column_device.set_segment_colors(
+ [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0)],
+ )
+ time.sleep(3)
+ left_column_device.set_segment_colors(
+ [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0), Color(r=0, g=0, b=25)],
+ )
+ time.sleep(3)
+
+ left_column_device.terminate_segment()
+
+ except Exception as e:
+ print(e)
+ finally:
+ left_column_device.power_off()
+
+
+def capture_screen_and_process_colors() -> np.ndarray:
+ screen_image = ImageGrab.grab()
+ resized_image = screen_image.resize(
+ (MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT)
+ )
+
+ # Increase the saturation
+ converter = ImageEnhance.Color(resized_image)
+ saturated_image = converter.enhance(1.5)
+
+ color_matrix = np.array(saturated_image).transpose(1, 0, 2)
+
+ # Replace colors that are too dark
+ color_sums = color_matrix.sum(axis=2)
+ dark_colors = color_sums < 10
+ color_matrix[dark_colors] = [10, 10, 10]
+
+ return color_matrix
+
+
+def game_loop(devices: list[GoveeLightDevice]):
+ try:
+ for device in devices:
+ device.power_on()
+ time.sleep(0.1)
+ device.set_brightness(100)
+ time.sleep(0.1)
+ device.set_color(Color(r=55, g=165, b=0))
+
+ time.sleep(0.5)
+
+ for device in devices:
+ device.initialize_segment()
+
+ while True:
+ screen_colors = capture_screen_and_process_colors()
+ for device in devices:
+ color_data = [
+ Color(r=r, g=g, b=b)
+ for r, g, b in screen_colors[*device.screen_positions]
+ ]
+ device.set_segment_colors(color_data)
+ time.sleep(0.005) # 200 FPS
+ except KeyboardInterrupt:
+ print("Operation stopped by user.")
+ finally:
+ for device in devices:
+ device.terminate_segment()
+ time.sleep(0.5)
+ device.power_off()
+
+
+def run():
+ left_column_device = GoveeLightDevice(
+ "192.168.0.248", "Govee Light Left", get_column_indices(0)
+ )
+ right_column_device = GoveeLightDevice(
+ "192.168.0.108",
+ "Govee Light Right",
+ get_column_indices(MAX_LED_COLOR_GRADIENT - 1),
+ )
+ devices = [left_column_device, right_column_device]
+
+ def power_off_devices_and_exit():
+ if left_column_device is not None:
+ left_column_device.power_off()
+ if right_column_device is not None:
+ right_column_device.power_off()
+ exit()
+
+ print("Starting Govee Light Controller...")
+ signal.signal(signal.SIGTERM, lambda signum, frame: power_off_devices_and_exit())
+ signal.signal(signal.SIGINT, lambda signum, frame: power_off_devices_and_exit())
+ game_loop(devices=devices)
+
+
+if __name__ == "__main__":
+ run()
From f2c638911435a16116683e329c770f664acd1a70 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sat, 13 Apr 2024 14:06:52 -0400
Subject: [PATCH 03/11] better screen capture logic
---
ScreenSync.py | 149 ++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 126 insertions(+), 23 deletions(-)
diff --git a/ScreenSync.py b/ScreenSync.py
index 9f3a754..b277555 100644
--- a/ScreenSync.py
+++ b/ScreenSync.py
@@ -6,14 +6,20 @@
from typing import Union
import numpy as np
-from PIL import ImageEnhance, ImageGrab
+from PIL import Image, ImageEnhance, ImageGrab
from pydantic import BaseModel, Field
+DEBUG = False
UDP_PORT = 4003
MAX_LED_COLOR_GRADIENT = 10
# MAX_LED_COLOR_GRADIENT = 4
+def show_image(image: np.ndarray):
+ img = Image.fromarray(image.astype(np.uint8), "RGB")
+ img.show()
+
+
class PowerState(Enum):
OFF = 0
ON = 1
@@ -21,9 +27,9 @@ class PowerState(Enum):
# Pydantic Models
class Color(BaseModel):
- r: int = Field(..., ge=0, le=255)
- g: int = Field(..., ge=0, le=255)
- b: int = Field(..., ge=0, le=255)
+ r: int = Field(default=205, ge=0, le=255)
+ g: int = Field(default=125, ge=0, le=255)
+ b: int = Field(default=0, ge=0, le=255)
@property
def rgb(self):
@@ -151,7 +157,9 @@ def _get_segment_color_data(
return SegmentData(pt=final_send_value)
-def get_column_indices(column_number: int):
+def get_device_location_indices(
+ column_indices: list[int] | None = None, row_indices: list[int] | None = None
+):
"""
screen example for MAX_LED_COLOR_GRADIENT = 10
@@ -170,9 +178,12 @@ def get_column_indices(column_number: int):
My device has index 0 at the bottom, so I need to reverse the row indices.
"""
- column_indices = [column_number] * MAX_LED_COLOR_GRADIENT
- row_indices = list(range(MAX_LED_COLOR_GRADIENT))[::-1]
- screen_indices = [column_indices, row_indices]
+ if column_indices is None:
+ column_indices = list(range(MAX_LED_COLOR_GRADIENT))
+ if row_indices is None:
+ # light lamp colors are ordered from bottom to top
+ row_indices = list(range(MAX_LED_COLOR_GRADIENT)[::-1])
+ screen_indices = [row_indices, column_indices]
return screen_indices
@@ -219,33 +230,117 @@ def example_usage():
def capture_screen_and_process_colors() -> np.ndarray:
+ # Capture the entire screen
screen_image = ImageGrab.grab()
+ resize_factor = 10
+
+ # Resize image to a manageable size that maintains the aspect ratio of 10x10 blocks
resized_image = screen_image.resize(
- (MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT)
+ (MAX_LED_COLOR_GRADIENT * resize_factor, MAX_LED_COLOR_GRADIENT * resize_factor)
)
- # Increase the saturation
+ # Increase the saturation by 1.5 times
converter = ImageEnhance.Color(resized_image)
- saturated_image = converter.enhance(1.5)
+ saturated_image = converter.enhance(2.5)
+
+ # Convert image to numpy array for processing
+ color_matrix = np.array(saturated_image)
+
+ # Reshape the array to (10, 10, 10, 10, 3) where each 10x10 block's pixels are separately accessible
+ color_matrix = color_matrix.reshape(
+ (
+ MAX_LED_COLOR_GRADIENT,
+ resize_factor,
+ MAX_LED_COLOR_GRADIENT,
+ resize_factor,
+ 3,
+ )
+ )
+
+ # I favor red > green > blue for the most "colorful pixel". Let's scale the colors accordingly
+ biased_color_matrix = color_matrix.copy()
+ biased_color_matrix[..., 0] *= 2
+ biased_color_matrix[..., 1] *= 1
+ biased_color_matrix[..., 2] *= 1
+
+ # Compute variance across each pixel to find the most "colorful" pixel
+ pixel_variances = np.var(biased_color_matrix, axis=4)
+
+ # Transpose the array so that the 10x10 blocks are along the last two axes
+ pixel_variances_transposed = pixel_variances.transpose(0, 2, 1, 3)
+
+ # Flatten the last two dimensions
+ pixel_variances_flattened = pixel_variances_transposed.reshape(
+ MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT, -1
+ )
+
+ # Compute argmax over the flattened dimension
+ max_colorful_indices = np.unravel_index(
+ pixel_variances_flattened.argmax(axis=2), (resize_factor, resize_factor)
+ )
+
+ # For each block, get the most colorful pixel
+ most_colorful_pixels = color_matrix[
+ np.arange(MAX_LED_COLOR_GRADIENT)[:, None],
+ max_colorful_indices[0],
+ np.arange(MAX_LED_COLOR_GRADIENT),
+ max_colorful_indices[1],
+ ]
- color_matrix = np.array(saturated_image).transpose(1, 0, 2)
+ most_colorful_pixels = most_colorful_pixels.reshape(
+ MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT, 3
+ )
- # Replace colors that are too dark
- color_sums = color_matrix.sum(axis=2)
- dark_colors = color_sums < 10
- color_matrix[dark_colors] = [10, 10, 10]
+ if DEBUG:
+ saturated_image.save("debug_saturated_image.png")
- return color_matrix
+ most_colorful_pixels_image = Image.fromarray(
+ most_colorful_pixels.astype(np.uint8), "RGB"
+ )
+ most_colorful_pixels_image.save("debug_preview.png")
+ # raise an exception to stop the program and show the image
+ raise Exception("Debug mode enabled. Stopping program to show image.")
+
+ return most_colorful_pixels
+
+
+class FrameCounter:
+ def __init__(self):
+ self.last_100_fps = []
+ self.start_time = time.time()
+ self.previous_time = time.time()
+
+ def update_and_print(self):
+ elapsed_time = time.time() - self.previous_time
+ self.previous_time = time.time()
+ time_since_start = time.time() - self.start_time
+ fps = 1 / elapsed_time
+ self.last_100_fps = self.last_100_fps[-99:]
+ self.last_100_fps.append(fps)
+ average_fps = (
+ sum(self.last_100_fps) / len(self.last_100_fps) if self.last_100_fps else 0
+ )
+ print(
+ f"\rFPS: {fps:.1f} - Average FPS {average_fps:.1f} - Total Time:{time_since_start:.1f}",
+ end="",
+ )
+
+ return fps
+
+ def reset(self):
+ self.frame_count = 0
+ self.start_time = time.time()
def game_loop(devices: list[GoveeLightDevice]):
+ frame_counter = FrameCounter()
try:
for device in devices:
device.power_on()
time.sleep(0.1)
device.set_brightness(100)
time.sleep(0.1)
- device.set_color(Color(r=55, g=165, b=0))
+ device.set_color(Color())
time.sleep(0.5)
@@ -255,12 +350,17 @@ def game_loop(devices: list[GoveeLightDevice]):
while True:
screen_colors = capture_screen_and_process_colors()
for device in devices:
+ selected_rows = screen_colors[device.screen_positions[0], :, :]
+ selected_columns = selected_rows[:, device.screen_positions[1], :]
color_data = [
Color(r=r, g=g, b=b)
- for r, g, b in screen_colors[*device.screen_positions]
+ for r, g, b in selected_columns.mean(axis=1).astype(int)
]
device.set_segment_colors(color_data)
- time.sleep(0.005) # 200 FPS
+ frame_counter.update_and_print()
+ time.sleep(1 / 145) # 144 Hz
+ # write to terminal a fps counter (but don't print it every frame, instead update it in place)
+
except KeyboardInterrupt:
print("Operation stopped by user.")
finally:
@@ -270,14 +370,17 @@ def game_loop(devices: list[GoveeLightDevice]):
device.power_off()
+np.take
+
+
def run():
left_column_device = GoveeLightDevice(
- "192.168.0.248", "Govee Light Left", get_column_indices(0)
+ "192.168.0.248", "Govee Light Right", get_device_location_indices([7, 8, 9])
)
right_column_device = GoveeLightDevice(
"192.168.0.108",
- "Govee Light Right",
- get_column_indices(MAX_LED_COLOR_GRADIENT - 1),
+ "Govee Light Left",
+ get_device_location_indices([0, 1, 2]),
)
devices = [left_column_device, right_column_device]
From 95c1e83be934d68231fa892055d6fdccc6e036c4 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sat, 13 Apr 2024 14:13:01 -0400
Subject: [PATCH 04/11] Update ScreenSync.py
---
ScreenSync.py | 21 ++++++++-------------
1 file changed, 8 insertions(+), 13 deletions(-)
diff --git a/ScreenSync.py b/ScreenSync.py
index b277555..429d2cc 100644
--- a/ScreenSync.py
+++ b/ScreenSync.py
@@ -12,12 +12,6 @@
DEBUG = False
UDP_PORT = 4003
MAX_LED_COLOR_GRADIENT = 10
-# MAX_LED_COLOR_GRADIENT = 4
-
-
-def show_image(image: np.ndarray):
- img = Image.fromarray(image.astype(np.uint8), "RGB")
- img.show()
class PowerState(Enum):
@@ -25,7 +19,6 @@ class PowerState(Enum):
ON = 1
-# Pydantic Models
class Color(BaseModel):
r: int = Field(default=205, ge=0, le=255)
g: int = Field(default=125, ge=0, le=255)
@@ -93,7 +86,8 @@ def _send_command(self, command: Command):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.sendto(message.encode(), (self.ip, UDP_PORT))
- # print(f"Command sent to {ip}: {message}")
+ if DEBUG:
+ print(f"Command sent to {self.ip}: {message}")
def power_on(self):
power_on_command = PowerCommand(data=PowerData(value=PowerState.ON))
@@ -143,6 +137,12 @@ def set_segment_colors(self, list_of_colors: list[Color], gradient=True):
def _get_segment_color_data(
self, list_of_colors: list[Color], gradient=True
) -> SegmentData:
+ """Returns the SegmentData object for the given list of colors.
+
+ The segment data is a base64 encoded string that represents the colors to be set.
+ I have no idea what the values for the first 4 bytes are.
+ I assume the 6th byte is the number of colors.
+ """
gradient_flag = 1 if gradient else 0
byte_array = [187, 0, 32, 176, gradient_flag, len(list_of_colors)]
@@ -189,7 +189,6 @@ def get_device_location_indices(
def example_usage():
default_color = Color(r=255, g=165, b=0)
- # controller = LightController(["192.168.0.108", "192.168.0.248"])
left_column_screen_indices = [(0, i) for i in range(MAX_LED_COLOR_GRADIENT)]
left_column_device = GoveeLightDevice(
"192.168.0.248", "Govee Light Left", left_column_screen_indices
@@ -358,7 +357,6 @@ def game_loop(devices: list[GoveeLightDevice]):
]
device.set_segment_colors(color_data)
frame_counter.update_and_print()
- time.sleep(1 / 145) # 144 Hz
# write to terminal a fps counter (but don't print it every frame, instead update it in place)
except KeyboardInterrupt:
@@ -370,9 +368,6 @@ def game_loop(devices: list[GoveeLightDevice]):
device.power_off()
-np.take
-
-
def run():
left_column_device = GoveeLightDevice(
"192.168.0.248", "Govee Light Right", get_device_location_indices([7, 8, 9])
From a73c4799f4507eb139b04a602eaef8293c05314f Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:36:23 -0400
Subject: [PATCH 05/11] package as poetry project
---
GameSync2024.py | 262 ------------
GameSync2024v2.py | 176 --------
README.md | 45 ---
old/BasicControls.py | 22 +
GameSync.py => old/GameSync.py | 378 +++++++++---------
GameSync2023.py => old/GameSync2023.py | 356 ++++++++---------
old/GameSync2024.py | 335 ++++++++++++++++
old/GameSync2024v2.py | 138 +++++++
old/GetGoveeDevices.py | 56 +++
old/README.md | 45 +++
ScreenSync.py => old/ScreenSync.py | 5 +
UDPReceiver.py => old/UDPReceiver.py | 36 +-
UDPSender.py => old/UDPSender.py | 48 +--
old/controller.py | 30 ++
old/debug_console.png | Bin 0 -> 106 bytes
old/debug_preview.png | Bin 0 -> 352 bytes
old/debug_saturated_image.png | Bin 0 -> 9073 bytes
old/models.py | 298 ++++++++++++++
old/stuff.md | 12 +
poetry.lock | 295 ++++++++++++++
pyproject.toml | 71 ++++
src/govee_screen_sync/__init__.py | 0
.../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 168 bytes
.../__pycache__/config.cpython-312.pyc | Bin 0 -> 243 bytes
.../__pycache__/game_sync.cpython-312.pyc | Bin 0 -> 3877 bytes
.../__pycache__/light_device.cpython-312.pyc | Bin 0 -> 7162 bytes
.../__pycache__/models.cpython-312.pyc | Bin 0 -> 4365 bytes
.../screen_capture.cpython-312.pyc | Bin 0 -> 2678 bytes
.../__pycache__/utils.cpython-312.pyc | Bin 0 -> 5451 bytes
src/govee_screen_sync/config.py | 3 +
src/govee_screen_sync/game_sync.py | 64 +++
src/govee_screen_sync/light_device.py | 132 ++++++
src/govee_screen_sync/main.py | 49 +++
src/govee_screen_sync/models.py | 82 ++++
src/govee_screen_sync/screen_capture.py | 79 ++++
src/govee_screen_sync/utils.py | 109 +++++
36 files changed, 2234 insertions(+), 892 deletions(-)
delete mode 100644 GameSync2024.py
delete mode 100644 GameSync2024v2.py
create mode 100644 old/BasicControls.py
rename GameSync.py => old/GameSync.py (97%)
rename GameSync2023.py => old/GameSync2023.py (97%)
create mode 100644 old/GameSync2024.py
create mode 100644 old/GameSync2024v2.py
create mode 100644 old/GetGoveeDevices.py
create mode 100644 old/README.md
rename ScreenSync.py => old/ScreenSync.py (99%)
rename UDPReceiver.py => old/UDPReceiver.py (96%)
rename UDPSender.py => old/UDPSender.py (96%)
create mode 100644 old/controller.py
create mode 100644 old/debug_console.png
create mode 100644 old/debug_preview.png
create mode 100644 old/debug_saturated_image.png
create mode 100644 old/models.py
create mode 100644 old/stuff.md
create mode 100644 poetry.lock
create mode 100644 pyproject.toml
create mode 100644 src/govee_screen_sync/__init__.py
create mode 100644 src/govee_screen_sync/__pycache__/__init__.cpython-312.pyc
create mode 100644 src/govee_screen_sync/__pycache__/config.cpython-312.pyc
create mode 100644 src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc
create mode 100644 src/govee_screen_sync/__pycache__/light_device.cpython-312.pyc
create mode 100644 src/govee_screen_sync/__pycache__/models.cpython-312.pyc
create mode 100644 src/govee_screen_sync/__pycache__/screen_capture.cpython-312.pyc
create mode 100644 src/govee_screen_sync/__pycache__/utils.cpython-312.pyc
create mode 100644 src/govee_screen_sync/config.py
create mode 100644 src/govee_screen_sync/game_sync.py
create mode 100644 src/govee_screen_sync/light_device.py
create mode 100644 src/govee_screen_sync/main.py
create mode 100644 src/govee_screen_sync/models.py
create mode 100644 src/govee_screen_sync/screen_capture.py
create mode 100644 src/govee_screen_sync/utils.py
diff --git a/GameSync2024.py b/GameSync2024.py
deleted file mode 100644
index 2785bea..0000000
--- a/GameSync2024.py
+++ /dev/null
@@ -1,262 +0,0 @@
-from threading import Thread
-import ctypes
-from ctypes import *
-import ctypes.wintypes
-import json
-import socket
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-import sys,os
-import base64
-import time
-DeviceIP = ["10.1.1.54","10.1.1.43","10.1.1.44"] #INSERT YOUR DEVICE IP(S)
-
-def GoveeLocalControl(Command,brightness=0,color=None,Loop=False,UDP_IP='',UDP_PORT=4003,printStatus=True,errorCount=0):
- lastColor = None
- global sock
-
- def SendCommand(message,individualIP,UDP_PORT):
- jsonResult = json.dumps(message)
- # jsonResult = jsonResult.replace("'",'"')
- print("Sending: {} to {} - {}".format(Command,individualIP,jsonResult))
- sock.sendto(bytes(jsonResult, "utf-8"), (individualIP, UDP_PORT))
-
- try:
- if Command == "Color":
- message = {
- "msg":{
- "cmd":"colorwc",
- "data":{
- "color":{"r":color[0],"g":color[1],"b":color[2]},
- "colorTemInKelvin":0
- }
- }
- }
- elif Command == "On":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":1,
- }
- }
- }
- elif Command == "Off":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":0,
- }
- }
- }
- elif Command == "SegmentInit":
- message = {
- "msg":{
- "cmd":"status",
- "data":{}
- }
- }
- for individualIP in UDP_IP:
- SendCommand(message,individualIP,UDP_PORT)
- message = {
- "msg":{
- "cmd":"razer",
- "data":{
- 'pt':'uwABsQEK',
- }
- }
- }
- for individualIP in UDP_IP:
- SendCommand(message,individualIP,UDP_PORT)
- message = {
- "msg":{
- "cmd":"status",
- "data":{}
- }
- }
- time.sleep(2)
- elif Command == "SegmentTerm":
- message = {
- "msg":{
- "cmd":"status",
- "data":{}
- }
- }
- for individualIP in UDP_IP:
- SendCommand(message,individualIP,UDP_PORT)
- message = {
- "msg":{
- "cmd":"razer",
- "data":{
- 'pt':'uwABsQAL',
- }
- }
- }
- for individualIP in UDP_IP:
- SendCommand(message,individualIP,UDP_PORT)
- message = {
- "msg":{
- "cmd":"status",
- "data":{}
- }
- }
- time.sleep(2)
- elif Command == "SegmentColor":
- gradientFlag = 1
- r=color[0][0]
- g=color[0][1]
- b=color[0][2]
- gradientFlag = 1
- # byteArray = [187,0,32,176,gradientFlag,10,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b]
- byteArray = [187,0,32,176,gradientFlag,10,color[0][0],color[0][1],color[0][2],color[0][0],color[0][1],color[0][2],color[0][0],color[0][1],color[0][2],color[0][0],color[0][1],color[0][2],color[0][0],color[0][1],color[0][2],color[1][0],color[1][1],color[1][2],color[1][0],color[1][1],color[1][2],color[1][0],color[1][1],color[1][2],color[1][0],color[1][1],color[1][2],color[1][0],color[1][1],color[1][2]]
- num2 = 0
- for byte in byteArray:
- num2 ^= byte
- byteArray.append(num2)
- finalSendValue = (base64.b64encode(bytes(byteArray)).decode())
- message = {
- "msg":{
- "cmd":"razer",
- "data":{
- 'pt':finalSendValue,
- }
- }
- }
- elif Command == "BrightLevel":
- message = {
- "msg":{
- "cmd":"brightness",
- "data":{
- "value":brightness,
- }
- }
- }
- else:
- raise ValueError("Bad Command: {}".format(Command))
-
- #To print or not to print
- if Loop == False and printStatus == True:
- print("Govee Internal Control: {}".format(Command))
-
- #Send command from above
- for individualIP in UDP_IP:
- SendCommand(message,individualIP,UDP_PORT)
- except Exception as E:
- errorCount += 1
- if errorCount > 3:
- print("Govee Internal Control Error Recursion Exit: {}".format(str(E)))
- return
- else:
- print("Govee Internal Control Error: {}".format(str(E)))
- return GoveeLocalControl(Command,brightness,color,Loop,UDP_IP,UDP_PORT,printStatus,errorCount)
-
-def main(DeviceIP):
- global boxSize
- global sock
- errorNotified = False
- lastColor = None
- handle = None
- whndl = None
- user32 = ctypes.windll.user32
- gdi32 = ctypes.windll.gdi32
-
- screen_width = user32.GetSystemMetrics(0)
- screen_height = user32.GetSystemMetrics(1)
- print("Using Screen Dimensions: {} {}".format(screen_width,screen_height))
- middle_x = screen_width // 2
- middle_y = screen_height // 2
- chunk = int(screen_width / 4)
-
- # Get the device context for the entire screen
- screen_dc = user32.GetDC(None)
-
- # Create a compatible device context
- mem_dc = gdi32.CreateCompatibleDC(screen_dc)
-
- # Create a bitmap compatible with the screen device context
- bmp = gdi32.CreateCompatibleBitmap(screen_dc, 1, 1)
-
- # Select the bitmap object into the compatible device context
- gdi32.SelectObject(mem_dc, bmp)
-
- # Define the value of SRCCOPY manually
- SRCCOPY = 0xCC0020
-
- try:
- print("Game Time Function: Turning Lights On")
- while(1):
- # Copy the pixel color from the screen to the bitmap
- gdi32.BitBlt(mem_dc, 0, 0, 1, 1, screen_dc, middle_x-chunk, middle_y, SRCCOPY)
-
- # Retrieve the color value of the pixel
- pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
-
- # Extract the individual color components (red, green, blue)
- red = pixel_color & 0xFF
- green = (pixel_color >> 8) & 0xFF
- blue = (pixel_color >> 16) & 0xFF
- pixelColor1 = (red,green,blue)
-
- gdi32.BitBlt(mem_dc, 0, 0, 1, 1, screen_dc, middle_x+chunk, middle_y, SRCCOPY)
-
- # Retrieve the color value of the pixel
- pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
-
- # Extract the individual color components (red, green, blue)
- red = pixel_color & 0xFF
- green = (pixel_color >> 8) & 0xFF
- blue = (pixel_color >> 16) & 0xFF
- pixelColor2 = (red,green,blue)
- # print(str(middle_x-chunk)+"-"+str(pixelColor1))
- # print(str(middle_x+chunk)+"-"+str(pixelColor2))
-
- try:
- #Send colors
- if "Error" in pixelColor1 or "Error" in pixelColor2:
- if errorNotified == False:
- print("Error In Colors Loop: {}".format(pixelColor))
- errorNotified = True
- time.sleep(5)
- continue
- else:
- # print(pixelColor)
- lastColor = pixelColor1
- errorNotified = False
- GoveeLocalControl(Command="SegmentColor",UDP_IP=DeviceIP,color=(pixelColor1,pixelColor2))
- except Exception as E:
- print("Govee Game Time Inner Loop Error: {}".format(str(E)))
- #If for some reason we break from loop and we return from this thread, we want to power down lights.
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- gdi32.DeleteDC(mem_dc)
- gdi32.DeleteDC(screen_dc)
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- raise NameError("NormalReturn")
- except Exception as E:
- if "NormalReturn" in str(E):
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- return "NormalExit"
- else:
- print("Govee Game Time Loop Error: {}".format(str(E)))
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- return "Error"
-
-try:
- print("Press Ctrl + C To Quit (Lights Should Turn Off)\n\n")
- GoveeLocalControl(Command="SegmentTerm",UDP_IP=DeviceIP)
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- time.sleep(2)
- GoveeLocalControl(Command="On",UDP_IP=DeviceIP)
- GoveeLocalControl(Command="BrightLevel",UDP_IP=DeviceIP,brightness=100)
- time.sleep(2)
- GoveeLocalControl(Command="SegmentInit",UDP_IP=DeviceIP)
- time.sleep(5)
- GoveeLocalControl(Command="SegmentColor",color=(255,0,0),UDP_IP=DeviceIP)
- main(DeviceIP)
-except KeyboardInterrupt:
- GoveeLocalControl(Command="SegmentTerm",UDP_IP=DeviceIP)
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- try:
- sys.exit(130)
- except SystemExit:
- os._exit(130)
-
diff --git a/GameSync2024v2.py b/GameSync2024v2.py
deleted file mode 100644
index c0cf73a..0000000
--- a/GameSync2024v2.py
+++ /dev/null
@@ -1,176 +0,0 @@
-import base64
-import socket
-import time
-from enum import Enum
-
-from PIL import ImageGrab
-from pydantic import BaseModel, Field, validator
-
-# Configuration
-# DEVICE_IP: list[str] = ["10.1.1.54", "10.1.1.43"] # Example IPs for two devices
-DEVICE_IP: list[str] = ["192.168.0.108", "192.168.0.248"]
-UDP_PORT: int = 4003 # Default UDP port
-NUM_COLORS: int = 20 # Number of color samples per device
-
-
-class PowerState(Enum):
- OFF = 0
- ON = 1
-
-
-class Color(BaseModel):
- r: int = Field(..., ge=0, le=255)
- g: int = Field(..., ge=0, le=255)
- b: int = Field(..., ge=0, le=255)
-
-
-class Command(BaseModel):
- msg: dict
-
-
-class PowerCommand(Command):
- msg: dict = {"cmd": "turn", "data": {"value": PowerState.OFF.value}}
-
- @validator("msg", pre=True)
- def set_power_state(cls, value, values):
- value["data"]["value"] = values.get("power_state", PowerState.OFF).value
- return value
-
-
-class BrightnessCommand(Command):
- msg: dict = {"cmd": "brightness", "data": {"value": 100}}
-
- @validator("msg", pre=True)
- def set_brightness(cls, value, values):
- value["data"]["value"] = values.get("brightness", 100)
- return value
-
-
-class ColorCommand(Command):
- msg: dict = {
- "cmd": "colorwc",
- "data": {"color": {"r": 255, "g": 255, "b": 255}, "colorTemInKelvin": 0},
- }
-
- @validator("msg", pre=True)
- def set_color(cls, value, values):
- color = values.get("color", Color(r=255, g=255, b=255))
- value["data"]["color"] = color.dict()
- return value
-
-
-class SegmentCommand(Command):
- msg: dict = {"cmd": "razer", "data": {"pt": "uwABsQEK"}}
-
- @validator("msg", pre=True)
- def set_segment(cls, value, values):
- value["data"]["pt"] = values.get("segment", "uwABsQEK")
- return value
-
-
-class FormatAndSendColorsCommand(Command):
- msg: dict = {"cmd": "razer", "data": {"pt": ""}}
-
- @validator("msg", pre=True)
- def set_colors(cls, value, values):
- colors = values.get("colors", [])
- byteArray: list[int] = [187, 0, 32, 176, 1, NUM_COLORS] + [
- c for color in colors for c in color
- ]
- checksum: int = sum(byteArray) % 256
- byteArray.append(checksum)
- encoded_data: str = base64.b64encode(bytes(byteArray)).decode()
- value["data"]["pt"] = encoded_data
- return value
-
-
-def send_command(udp_ip: str, command: Command) -> None:
- """Send command data to a specific IP using UDP."""
- with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
- sock.sendto(command.json().encode(), (udp_ip, UDP_PORT))
- print(f"Command sent to {udp_ip}")
-
-
-def capture_screen_and_process_colors(
- num_devices: int,
-) -> list[list[tuple[int, int, int]]]:
- """Capture screen, compute average colors for specified points, and return them."""
- screen_image = ImageGrab.grab() # Capture the entire screen
- screen_width, screen_height = screen_image.size
-
- # Define sample positions on the screen
- vertical_positions: list[int] = [
- int(screen_height * i / NUM_COLORS) for i in range(NUM_COLORS)
- ]
-
- # Calculate horizontal sections for each device
- horizontal_sections: list[int] = [
- int(screen_width * (i + 1) / (num_devices + 1)) for i in range(num_devices)
- ]
-
- device_colors: list[list[tuple[int, int, int]]] = []
- for x in horizontal_sections:
- section_colors: list[tuple[int, int, int]] = [
- screen_image.getpixel((x, y)) for y in vertical_positions
- ]
- device_colors.append(section_colors)
-
- return device_colors
-
-
-def game_loop() -> None:
- """Main game loop to handle screen capture, color processing, and device control."""
- try:
- for ip in DEVICE_IP:
- send_command(
- ip, PowerCommand(power_state=PowerState.ON)
- ) # Turn on the devices
- send_command(
- ip, BrightnessCommand(brightness=100)
- ) # Set brightness to 100%
- send_command(ip, SegmentCommand(segment="uwABsQEK")) # Initialize segments
- time.sleep(2)
-
- while True:
- color_data: list[list[tuple[int, int, int]]] = (
- capture_screen_and_process_colors(len(DEVICE_IP))
- )
- for ip, colors in zip(DEVICE_IP, color_data):
- send_command(ip, FormatAndSendColorsCommand(colors=colors))
- time.sleep(1) # Update interval
-
- except KeyboardInterrupt:
- print("Operation stopped by user.")
- finally:
- for ip in DEVICE_IP:
- send_command(
- ip, ColorCommand(color=Color(r=255, g=165, b=0))
- ) # Set light sunset color (orange)
- print("Setting light to orange...")
- time.sleep(1)
- for ip in DEVICE_IP:
- print("Turning off devices...")
- send_command(ip, SegmentCommand(segment="uwABsQAL")) # Terminate segments
- send_command(
- ip, PowerCommand(power_state=PowerState.OFF)
- ) # Turn off the devices
-
-
-def main() -> None:
- """Main function to handle the overall operation."""
- print("Press Ctrl+C to quit.")
- game_loop()
-
-
-if __name__ == "__main__":
- try:
- main()
- except Exception as e:
- print(f"An error occurred: {e}")
- finally:
- for ip in DEVICE_IP:
- print("Turning off devices...")
- send_command(ip, SegmentCommand(segment="uwABsQAL")) # Terminate segments
- send_command(
- ip, PowerCommand(power_state=PowerState.OFF)
- ) # Turn off the devices
diff --git a/README.md b/README.md
index e142368..e69de29 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +0,0 @@
-# GoveeSync
-This code will allow you to use Govee internal UDP api to control your device as well as sync what is on the screen. You need to install the below libs before running:
-pip install wmi
-pip install pywin32
-
-# Constants
-Update the device IP of the device you want to control on line 21.
-
-# No Frills Screen Sync Script
-
-If you don't want to get into the different functions and you just want something simple you can run and have work, just run GameSync2023.py file.
-
-- Replace line 10 with your device IP, for example: DeviceIP = "192.168.0.1"
-- Open cmd prompt then simply run with - python c:\PathToFile\GameSync2023.py
-- You can simply do Ctrl + C to exit when you want to end the light sync.
-
-
-# Full Featured Code With Many Supported Commands - All of the below commands are implemented within GameSync.py
-
-To turn the device on/off:
-GoveeInternalControl("On")
-GoveeInternalControl("Off")
-
-To set the brightness:
-GoveeInternalControl("BrightLevel",100) #1-100% expressed as an integer
-GoveeInternalControl("BrightLevel",50) #1-100% expressed as an integer
-GoveeInternalControl("BrightLevel",10) #1-100% expressed as an integer
-
-To change the color:
-GoveeInternalControl("Color",color=(0,255,0)) #Color expressed as a RGB tuple
-GoveeInternalControl("Color",color=(255,0,0)) #Color expressed as a RGB tuple
-
-To go into game mode where the screen will sync to the device:
-Do note: in this mode the code will attempt to lock onto the window that is in focus.
There is a box size constant you can mess around with if you want to and see if you get better results.
This mode essentially works by creating a square in the center of the app's window. We sample a pixel at each of the four corners of the square, get the color values, then average those 4 color values together. That averaged out value is then sent to the Govee lights over UDP api.
-
-GameTime()
-
-# Testing / Searching for devices - This is optional if you want to search for available Govee LAN api devices on your network.
-Download UDPReceiver.py and UDPSender.py
-
-- Start CMD prompt, navigate to the folder where you downloaded the above files. Then type: python UDPReceiver.py
This should start a UDP multicast listener on port 4002
-- Once the listener is started open another CMD prompt. Navigate to the folder, and type: python UDPSender.py
This should output any Govee devices found to the shell.
-
-
-
diff --git a/old/BasicControls.py b/old/BasicControls.py
new file mode 100644
index 0000000..ffe45ce
--- /dev/null
+++ b/old/BasicControls.py
@@ -0,0 +1,22 @@
+from controller import LightController
+from models import Color, ColorCommand, ColorData, PowerCommand, PowerData, PowerState
+
+DeviceIP = ["192.168.0.108", "192.168.0.248"]
+
+
+def main():
+ controller = LightController(["192.168.0.108", "192.168.0.248"])
+ # Example usage of the PowerCommand model
+ power_on_command = PowerCommand(data=PowerData(value=PowerState.ON))
+ controller.send_command(power_on_command)
+
+ # Example usage of the ColorCommand model
+ color_command = ColorCommand(data=ColorData(color=Color(r=255, g=124, b=0)))
+ controller.send_command(color_command)
+
+ power_on_command = PowerCommand(data=PowerData(value=PowerState.OFF))
+ controller.send_command(power_on_command)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/GameSync.py b/old/GameSync.py
similarity index 97%
rename from GameSync.py
rename to old/GameSync.py
index 11d35d6..ccdc471 100644
--- a/GameSync.py
+++ b/old/GameSync.py
@@ -1,189 +1,189 @@
-# pywin32 must be installed
-# pip install wmi
-
-import win32gui
-import win32process
-import win32pdhutil
-import wmi
-import time
-import socket
-import json
-import math
-import threading
-from ctypes import *
-import ctypes.wintypes
-
-#Globals, you should not change these, leave them as is.
-appChange = True
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-
-#Constants, you should change these.
-GoveeDeviceIP = '10.1.1.43'
-boxSize = 50
-
-
-def GoveeInternalControl(Command,brightness=0,color=None,Loop=False,UDP_IP=GoveeDeviceIP,UDP_PORT=4003):
- lastColor = None
- global sock
- global stopLoop
-
- def SendCommand(message,UDP_IP,UDP_PORT):
- jsonResult = json.dumps(message)
- # print("Sending: {}".format(Command))
- sock.sendto(bytes(jsonResult, "utf-8"), (UDP_IP, UDP_PORT))
-
- try:
- if Command == "On":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":1,
- }
- }
- }
- elif Command == "Off":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":0,
- }
- }
- }
- elif Command == "Color":
- message = {
- "msg":{
- "cmd":"colorwc",
- "data":{
- "color":{"r":color[0],"g":color[1],"b":color[2]},
- "colorTemInKelvin":0
- }
- }
- }
- elif Command == "BrightLevel":
- message = {
- "msg":{
- "cmd":"brightness",
- "data":{
- "value":brightness,
- }
- }
- }
- else:
- raise ValueError("Bad Command: {}".format(Command))
-
- if Loop == False:
- print("\nGovee Internal Control: {}".format(Command))
-
- SendCommand(message,UDP_IP,UDP_PORT)
- except Exception as E:
- print("\nGovee Internal Control Error: {}".format(str(E)))
-
-def DetectFocusChange():
- global appChange
- currentPID = ""
- while(1):
- time.sleep(1.5)
- whndl = win32gui.GetForegroundWindow()
- tempPID = (win32process.GetWindowThreadProcessId(whndl))[1]
- if currentPID != tempPID:
- print("\nDetected Window Change: {}".format(tempPID))
- currentPID = tempPID
- appChange = True
-
-def GameTime():
- global appChange
- global boxSize
- lastColor = None
- pid = ""
-
- def GetColors(handle,upperLeft,lowerLeft,upperRight,lowerRight):
- try:
- def rgbint2rgbtuple(RGBint,skew,):
- red = RGBint & 255
- green = (RGBint >> 8) & 255
- blue = (RGBint >> 16) & 255
- return (red * skew,green * skew,blue *skew)
-
- pixels = [rgbint2rgbtuple(win32gui.GetPixel(handle,upperLeft[0],upperLeft[1]),1),
- rgbint2rgbtuple(win32gui.GetPixel(handle,lowerLeft[0],lowerLeft[1]),1),
- rgbint2rgbtuple(win32gui.GetPixel(handle,upperRight[0],upperRight[1]),1),
- rgbint2rgbtuple(win32gui.GetPixel(handle,lowerRight[0],lowerRight[1]),1)]
- red = 0
- green = 0
- blue = 0
- for pixel in pixels:
- red += pixel[0]**2
- green += pixel[1]**2
- blue += pixel[2]**2
- return int(math.sqrt(red/len(pixels))),int(math.sqrt(green/len(pixels))),int(math.sqrt(blue/len(pixels)))
- except Exception as E:
- # time.sleep(10)
- return "Get Colors Error: {}".format(str(E))
-
- try:
- GoveeInternalControl("On")
- GoveeInternalControl("BrightLevel",brightness=100)
- while(1):
- if appChange == True:
- handle = None
- whndl = None
- whndl = win32gui.GetForegroundWindow()
- pid =(win32process.GetWindowThreadProcessId(whndl))[1]
- handle = win32gui.GetWindowDC(whndl)
- txt = win32gui.GetWindowText(whndl)
- rect = ctypes.wintypes.RECT()
- DWMWA_EXTENDED_FRAME_BOUNDS = 9
- ctypes.windll.dwmapi.DwmGetWindowAttribute(
- ctypes.wintypes.HWND(whndl),
- ctypes.wintypes.DWORD(DWMWA_EXTENDED_FRAME_BOUNDS),
- ctypes.byref(rect),
- ctypes.sizeof(rect)
- )
- size = (rect.right - rect.left, rect.bottom - rect.top)
- windowWidth = size[0]
- windowHeight = size[1]
- centerWidth = int(windowWidth / 2)
- centerHeight = int(windowHeight / 2)
- upperLeft = (centerWidth-boxSize,centerHeight-boxSize)
- lowerLeft = (centerWidth-boxSize,centerHeight+boxSize)
- upperRight = (centerWidth+boxSize,centerHeight-boxSize)
- lowerRight = (centerWidth+boxSize,centerHeight+boxSize)
- print("\nFound New Focused Window:\nPid: {}\nWidth: {}\nHeight: {}\nupperLeft: {}\nlowerLeft: {}\nupperRight: {}\nlowerRight: {}".format(pid,windowWidth,windowHeight,upperLeft,lowerLeft,upperRight,lowerRight))
- appChange = False
-
- pixelColor = GetColors(handle,upperLeft,lowerLeft,upperRight,lowerRight)
- if pixelColor == lastColor:
- continue
- elif "Error" in pixelColor:
- print("\nError In Loop: {}".format(pixelColor))
- else:
- lastColor = pixelColor
- GoveeInternalControl("Color",color=pixelColor,Loop=True)
- GoveeInternalControl("Off")
- return "Done"
- except Exception as E:
- print("\nGame Time Loop Error: {}".format(str(E)))
- GoveeInternalControl("Off")
- return "Error"
-
-GoveeInternalControl("Off")
-
-appFocusThread = threading.Thread(name="AppThread",target=DetectFocusChange)
-appFocusThread.start()
-
-GameTime()
-# GoveeInternalControl("On")
-# time.sleep(2)
-# GoveeInternalControl("BrightLevel",100) #1-100% expressed as an integer
-# time.sleep(2)
-# GoveeInternalControl("Color",color=(255,0,0))
-# time.sleep(2)
-# GoveeInternalControl("BrightLevel",50) #1-100% expressed as an integer
-# time.sleep(2)
-# GoveeInternalControl("Color",color=(0,255,0))
-# time.sleep(2)
-# GoveeInternalControl("BrightLevel",10) #1-100% expressed as an integer
-# time.sleep(2)
-# GoveeInternalControl("Off")
+# pywin32 must be installed
+# pip install wmi
+
+import win32gui
+import win32process
+import win32pdhutil
+import wmi
+import time
+import socket
+import json
+import math
+import threading
+from ctypes import *
+import ctypes.wintypes
+
+#Globals, you should not change these, leave them as is.
+appChange = True
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+#Constants, you should change these.
+GoveeDeviceIP = '10.1.1.43'
+boxSize = 50
+
+
+def GoveeInternalControl(Command,brightness=0,color=None,Loop=False,UDP_IP=GoveeDeviceIP,UDP_PORT=4003):
+ lastColor = None
+ global sock
+ global stopLoop
+
+ def SendCommand(message,UDP_IP,UDP_PORT):
+ jsonResult = json.dumps(message)
+ # print("Sending: {}".format(Command))
+ sock.sendto(bytes(jsonResult, "utf-8"), (UDP_IP, UDP_PORT))
+
+ try:
+ if Command == "On":
+ message = {
+ "msg":{
+ "cmd":"turn",
+ "data":{
+ "value":1,
+ }
+ }
+ }
+ elif Command == "Off":
+ message = {
+ "msg":{
+ "cmd":"turn",
+ "data":{
+ "value":0,
+ }
+ }
+ }
+ elif Command == "Color":
+ message = {
+ "msg":{
+ "cmd":"colorwc",
+ "data":{
+ "color":{"r":color[0],"g":color[1],"b":color[2]},
+ "colorTemInKelvin":0
+ }
+ }
+ }
+ elif Command == "BrightLevel":
+ message = {
+ "msg":{
+ "cmd":"brightness",
+ "data":{
+ "value":brightness,
+ }
+ }
+ }
+ else:
+ raise ValueError("Bad Command: {}".format(Command))
+
+ if Loop == False:
+ print("\nGovee Internal Control: {}".format(Command))
+
+ SendCommand(message,UDP_IP,UDP_PORT)
+ except Exception as E:
+ print("\nGovee Internal Control Error: {}".format(str(E)))
+
+def DetectFocusChange():
+ global appChange
+ currentPID = ""
+ while(1):
+ time.sleep(1.5)
+ whndl = win32gui.GetForegroundWindow()
+ tempPID = (win32process.GetWindowThreadProcessId(whndl))[1]
+ if currentPID != tempPID:
+ print("\nDetected Window Change: {}".format(tempPID))
+ currentPID = tempPID
+ appChange = True
+
+def GameTime():
+ global appChange
+ global boxSize
+ lastColor = None
+ pid = ""
+
+ def GetColors(handle,upperLeft,lowerLeft,upperRight,lowerRight):
+ try:
+ def rgbint2rgbtuple(RGBint,skew,):
+ red = RGBint & 255
+ green = (RGBint >> 8) & 255
+ blue = (RGBint >> 16) & 255
+ return (red * skew,green * skew,blue *skew)
+
+ pixels = [rgbint2rgbtuple(win32gui.GetPixel(handle,upperLeft[0],upperLeft[1]),1),
+ rgbint2rgbtuple(win32gui.GetPixel(handle,lowerLeft[0],lowerLeft[1]),1),
+ rgbint2rgbtuple(win32gui.GetPixel(handle,upperRight[0],upperRight[1]),1),
+ rgbint2rgbtuple(win32gui.GetPixel(handle,lowerRight[0],lowerRight[1]),1)]
+ red = 0
+ green = 0
+ blue = 0
+ for pixel in pixels:
+ red += pixel[0]**2
+ green += pixel[1]**2
+ blue += pixel[2]**2
+ return int(math.sqrt(red/len(pixels))),int(math.sqrt(green/len(pixels))),int(math.sqrt(blue/len(pixels)))
+ except Exception as E:
+ # time.sleep(10)
+ return "Get Colors Error: {}".format(str(E))
+
+ try:
+ GoveeInternalControl("On")
+ GoveeInternalControl("BrightLevel",brightness=100)
+ while(1):
+ if appChange == True:
+ handle = None
+ whndl = None
+ whndl = win32gui.GetForegroundWindow()
+ pid =(win32process.GetWindowThreadProcessId(whndl))[1]
+ handle = win32gui.GetWindowDC(whndl)
+ txt = win32gui.GetWindowText(whndl)
+ rect = ctypes.wintypes.RECT()
+ DWMWA_EXTENDED_FRAME_BOUNDS = 9
+ ctypes.windll.dwmapi.DwmGetWindowAttribute(
+ ctypes.wintypes.HWND(whndl),
+ ctypes.wintypes.DWORD(DWMWA_EXTENDED_FRAME_BOUNDS),
+ ctypes.byref(rect),
+ ctypes.sizeof(rect)
+ )
+ size = (rect.right - rect.left, rect.bottom - rect.top)
+ windowWidth = size[0]
+ windowHeight = size[1]
+ centerWidth = int(windowWidth / 2)
+ centerHeight = int(windowHeight / 2)
+ upperLeft = (centerWidth-boxSize,centerHeight-boxSize)
+ lowerLeft = (centerWidth-boxSize,centerHeight+boxSize)
+ upperRight = (centerWidth+boxSize,centerHeight-boxSize)
+ lowerRight = (centerWidth+boxSize,centerHeight+boxSize)
+ print("\nFound New Focused Window:\nPid: {}\nWidth: {}\nHeight: {}\nupperLeft: {}\nlowerLeft: {}\nupperRight: {}\nlowerRight: {}".format(pid,windowWidth,windowHeight,upperLeft,lowerLeft,upperRight,lowerRight))
+ appChange = False
+
+ pixelColor = GetColors(handle,upperLeft,lowerLeft,upperRight,lowerRight)
+ if pixelColor == lastColor:
+ continue
+ elif "Error" in pixelColor:
+ print("\nError In Loop: {}".format(pixelColor))
+ else:
+ lastColor = pixelColor
+ GoveeInternalControl("Color",color=pixelColor,Loop=True)
+ GoveeInternalControl("Off")
+ return "Done"
+ except Exception as E:
+ print("\nGame Time Loop Error: {}".format(str(E)))
+ GoveeInternalControl("Off")
+ return "Error"
+
+GoveeInternalControl("Off")
+
+appFocusThread = threading.Thread(name="AppThread",target=DetectFocusChange)
+appFocusThread.start()
+
+GameTime()
+# GoveeInternalControl("On")
+# time.sleep(2)
+# GoveeInternalControl("BrightLevel",100) #1-100% expressed as an integer
+# time.sleep(2)
+# GoveeInternalControl("Color",color=(255,0,0))
+# time.sleep(2)
+# GoveeInternalControl("BrightLevel",50) #1-100% expressed as an integer
+# time.sleep(2)
+# GoveeInternalControl("Color",color=(0,255,0))
+# time.sleep(2)
+# GoveeInternalControl("BrightLevel",10) #1-100% expressed as an integer
+# time.sleep(2)
+# GoveeInternalControl("Off")
diff --git a/GameSync2023.py b/old/GameSync2023.py
similarity index 97%
rename from GameSync2023.py
rename to old/GameSync2023.py
index 94fc217..82d287c 100644
--- a/GameSync2023.py
+++ b/old/GameSync2023.py
@@ -1,179 +1,179 @@
-from threading import Thread
-import ctypes
-from ctypes import *
-import ctypes.wintypes
-import json
-import socket
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-import sys,os
-DeviceIP = "10.1.1.43" #INSERT YOUR DEVICE IP
-
-def GoveeLocalControl(Command,brightness=0,color=None,Loop=False,UDP_IP='',UDP_PORT=4003,printStatus=True,errorCount=0):
- lastColor = None
- global sock
-
- def SendCommand(message,individualIP,UDP_PORT):
- jsonResult = json.dumps(message)
- print("Sending: {} to {}".format(Command,individualIP))
- sock.sendto(bytes(jsonResult, "utf-8"), (individualIP, UDP_PORT))
-
- try:
- if Command == "Color":
- message = {
- "msg":{
- "cmd":"colorwc",
- "data":{
- "color":{"r":color[0],"g":color[1],"b":color[2]},
- "colorTemInKelvin":0
- }
- }
- }
- elif Command == "On":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":1,
- }
- }
- }
- elif Command == "Off":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":0,
- }
- }
- }
- elif Command == "BrightLevel":
- message = {
- "msg":{
- "cmd":"brightness",
- "data":{
- "value":brightness,
- }
- }
- }
- else:
- raise ValueError("Bad Command: {}".format(Command))
-
- #To print or not to print
- if Loop == False and printStatus == True:
- print("Govee Internal Control: {}".format(Command))
-
- #Send command from above
- SendCommand(message,UDP_IP,UDP_PORT)
- except Exception as E:
- errorCount += 1
- if errorCount > 3:
- print("Govee Internal Control Error Recursion Exit: {}".format(str(E)))
- return
- else:
- print("Govee Internal Control Error: {}".format(str(E)))
- return GoveeLocalControl(Command,brightness,color,Loop,UDP_IP,UDP_PORT,printStatus,errorCount)
-
-
-def main(DeviceIP):
- global boxSize
- global sock
- errorNotified = False
- lastColor = None
- handle = None
- whndl = None
- user32 = ctypes.windll.user32
- gdi32 = ctypes.windll.gdi32
-
- screen_width = user32.GetSystemMetrics(0)
- screen_height = user32.GetSystemMetrics(1)
- print("Using Screen Dimensions: {} {}".format(screen_width,screen_height))
- middle_x = screen_width // 2
- middle_y = screen_height // 2
-
- # Get the device context for the entire screen
- screen_dc = user32.GetDC(None)
-
- # Create a compatible device context
- mem_dc = gdi32.CreateCompatibleDC(screen_dc)
-
- # Create a bitmap compatible with the screen device context
- bmp = gdi32.CreateCompatibleBitmap(screen_dc, 1, 1)
-
- # Select the bitmap object into the compatible device context
- gdi32.SelectObject(mem_dc, bmp)
-
- # Define the value of SRCCOPY manually
- SRCCOPY = 0xCC0020
-
-
-
- try:
- print("Game Time Function: Turning Lights On")
- GoveeLocalControl(Command="On",UDP_IP=DeviceIP)
- GoveeLocalControl(Command="BrightLevel",UDP_IP=DeviceIP,brightness=100)
- while(1):
- # Copy the pixel color from the screen to the bitmap
- gdi32.BitBlt(mem_dc, 0, 0, 1, 1, screen_dc, middle_x, middle_y, SRCCOPY)
-
- # Retrieve the color value of the pixel
- pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
-
- # Extract the individual color components (red, green, blue)
- red = pixel_color & 0xFF
- green = (pixel_color >> 8) & 0xFF
- blue = (pixel_color >> 16) & 0xFF
- pixelColor = (red,green,blue)
-
- try:
- #Send colors
- if pixelColor == lastColor:
- # print("Same Color")
- continue
- elif "Error" in pixelColor:
- if errorNotified == False:
- print("Error In Colors Loop: {}".format(pixelColor))
- errorNotified = True
- time.sleep(5)
- continue
- else:
- # print(pixelColor)
- lastColor = pixelColor
- errorNotified = False
-
- message = {
- "msg":{
- "cmd":"colorwc",
- "data":{
- "color":{"r":red,"g":green,"b":blue},
- "colorTemInKelvin":0
- }
- }
- }
- # print("Sending: {}".format(Command))
- sock.sendto(bytes(json.dumps(message), "utf-8"), (DeviceIP, 4003))
- except Exception as E:
- print("Govee Game Time Inner Loop Error: {}".format(str(E)))
- #If for some reason we break from loop and we return from this thread, we want to power down lights.
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- gdi32.DeleteDC(mem_dc)
- gdi32.DeleteDC(screen_dc)
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- raise NameError("NormalReturn")
- except Exception as E:
- if "NormalReturn" in str(E):
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- return "NormalExit"
- else:
- print("Govee Game Time Loop Error: {}".format(str(E)))
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- return "Error"
-
-try:
- print("Press Ctrl + C To Quit (Lights Should Turn Off)\n\n")
- main(DeviceIP)
-except KeyboardInterrupt:
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- try:
- sys.exit(130)
- except SystemExit:
+from threading import Thread
+import ctypes
+from ctypes import *
+import ctypes.wintypes
+import json
+import socket
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+import sys,os
+DeviceIP = "10.1.1.43" #INSERT YOUR DEVICE IP
+
+def GoveeLocalControl(Command,brightness=0,color=None,Loop=False,UDP_IP='',UDP_PORT=4003,printStatus=True,errorCount=0):
+ lastColor = None
+ global sock
+
+ def SendCommand(message,individualIP,UDP_PORT):
+ jsonResult = json.dumps(message)
+ print("Sending: {} to {}".format(Command,individualIP))
+ sock.sendto(bytes(jsonResult, "utf-8"), (individualIP, UDP_PORT))
+
+ try:
+ if Command == "Color":
+ message = {
+ "msg":{
+ "cmd":"colorwc",
+ "data":{
+ "color":{"r":color[0],"g":color[1],"b":color[2]},
+ "colorTemInKelvin":0
+ }
+ }
+ }
+ elif Command == "On":
+ message = {
+ "msg":{
+ "cmd":"turn",
+ "data":{
+ "value":1,
+ }
+ }
+ }
+ elif Command == "Off":
+ message = {
+ "msg":{
+ "cmd":"turn",
+ "data":{
+ "value":0,
+ }
+ }
+ }
+ elif Command == "BrightLevel":
+ message = {
+ "msg":{
+ "cmd":"brightness",
+ "data":{
+ "value":brightness,
+ }
+ }
+ }
+ else:
+ raise ValueError("Bad Command: {}".format(Command))
+
+ #To print or not to print
+ if Loop == False and printStatus == True:
+ print("Govee Internal Control: {}".format(Command))
+
+ #Send command from above
+ SendCommand(message,UDP_IP,UDP_PORT)
+ except Exception as E:
+ errorCount += 1
+ if errorCount > 3:
+ print("Govee Internal Control Error Recursion Exit: {}".format(str(E)))
+ return
+ else:
+ print("Govee Internal Control Error: {}".format(str(E)))
+ return GoveeLocalControl(Command,brightness,color,Loop,UDP_IP,UDP_PORT,printStatus,errorCount)
+
+
+def main(DeviceIP):
+ global boxSize
+ global sock
+ errorNotified = False
+ lastColor = None
+ handle = None
+ whndl = None
+ user32 = ctypes.windll.user32
+ gdi32 = ctypes.windll.gdi32
+
+ screen_width = user32.GetSystemMetrics(0)
+ screen_height = user32.GetSystemMetrics(1)
+ print("Using Screen Dimensions: {} {}".format(screen_width,screen_height))
+ middle_x = screen_width // 2
+ middle_y = screen_height // 2
+
+ # Get the device context for the entire screen
+ screen_dc = user32.GetDC(None)
+
+ # Create a compatible device context
+ mem_dc = gdi32.CreateCompatibleDC(screen_dc)
+
+ # Create a bitmap compatible with the screen device context
+ bmp = gdi32.CreateCompatibleBitmap(screen_dc, 1, 1)
+
+ # Select the bitmap object into the compatible device context
+ gdi32.SelectObject(mem_dc, bmp)
+
+ # Define the value of SRCCOPY manually
+ SRCCOPY = 0xCC0020
+
+
+
+ try:
+ print("Game Time Function: Turning Lights On")
+ GoveeLocalControl(Command="On",UDP_IP=DeviceIP)
+ GoveeLocalControl(Command="BrightLevel",UDP_IP=DeviceIP,brightness=100)
+ while(1):
+ # Copy the pixel color from the screen to the bitmap
+ gdi32.BitBlt(mem_dc, 0, 0, 1, 1, screen_dc, middle_x, middle_y, SRCCOPY)
+
+ # Retrieve the color value of the pixel
+ pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
+
+ # Extract the individual color components (red, green, blue)
+ red = pixel_color & 0xFF
+ green = (pixel_color >> 8) & 0xFF
+ blue = (pixel_color >> 16) & 0xFF
+ pixelColor = (red,green,blue)
+
+ try:
+ #Send colors
+ if pixelColor == lastColor:
+ # print("Same Color")
+ continue
+ elif "Error" in pixelColor:
+ if errorNotified == False:
+ print("Error In Colors Loop: {}".format(pixelColor))
+ errorNotified = True
+ time.sleep(5)
+ continue
+ else:
+ # print(pixelColor)
+ lastColor = pixelColor
+ errorNotified = False
+
+ message = {
+ "msg":{
+ "cmd":"colorwc",
+ "data":{
+ "color":{"r":red,"g":green,"b":blue},
+ "colorTemInKelvin":0
+ }
+ }
+ }
+ # print("Sending: {}".format(Command))
+ sock.sendto(bytes(json.dumps(message), "utf-8"), (DeviceIP, 4003))
+ except Exception as E:
+ print("Govee Game Time Inner Loop Error: {}".format(str(E)))
+ #If for some reason we break from loop and we return from this thread, we want to power down lights.
+ print("Game Time Function: Turning Lights Off. Thread Is Dead.")
+ gdi32.DeleteDC(mem_dc)
+ gdi32.DeleteDC(screen_dc)
+ GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
+ raise NameError("NormalReturn")
+ except Exception as E:
+ if "NormalReturn" in str(E):
+ print("Game Time Function: Turning Lights Off. Thread Is Dead.")
+ return "NormalExit"
+ else:
+ print("Govee Game Time Loop Error: {}".format(str(E)))
+ GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
+ return "Error"
+
+try:
+ print("Press Ctrl + C To Quit (Lights Should Turn Off)\n\n")
+ main(DeviceIP)
+except KeyboardInterrupt:
+ GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
+ try:
+ sys.exit(130)
+ except SystemExit:
os._exit(130)
\ No newline at end of file
diff --git a/old/GameSync2024.py b/old/GameSync2024.py
new file mode 100644
index 0000000..8ff7a00
--- /dev/null
+++ b/old/GameSync2024.py
@@ -0,0 +1,335 @@
+import base64
+import ctypes
+import ctypes.wintypes
+import json
+import os
+import socket
+import sys
+import time
+from ctypes import *
+
+import numpy as np
+
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+DeviceIP = ["192.168.0.108", "192.168.0.248"]
+# DeviceIP = ["10.1.1.54","10.1.1.43","10.1.1.44"] #INSERT YOUR DEVICE IP(S)
+
+
+def GoveeLocalControl(
+ Command,
+ brightness=0,
+ color=None,
+ Loop=False,
+ UDP_IP="",
+ UDP_PORT=4003,
+ printStatus=False,
+ errorCount=0,
+):
+ lastColor = None
+ global sock
+
+ def SendCommand(message, individualIP, UDP_PORT):
+ jsonResult = json.dumps(message)
+ # jsonResult = jsonResult.replace("'",'"')
+ # print("Sending: {} to {} - {}".format(Command, individualIP, jsonResult))
+ sock.sendto(bytes(jsonResult, "utf-8"), (individualIP, UDP_PORT))
+
+ try:
+ if Command == "Color":
+ message = {
+ "msg": {
+ "cmd": "colorwc",
+ "data": {
+ "color": {"r": color[0], "g": color[1], "b": color[2]},
+ "colorTemInKelvin": 0,
+ },
+ }
+ }
+ elif Command == "On":
+ message = {
+ "msg": {
+ "cmd": "turn",
+ "data": {
+ "value": 1,
+ },
+ }
+ }
+ elif Command == "Off":
+ message = {
+ "msg": {
+ "cmd": "turn",
+ "data": {
+ "value": 0,
+ },
+ }
+ }
+ elif Command == "SegmentInit":
+ message = {"msg": {"cmd": "status", "data": {}}}
+ for individualIP in UDP_IP:
+ SendCommand(message, individualIP, UDP_PORT)
+ message = {
+ "msg": {
+ "cmd": "razer",
+ "data": {
+ "pt": "uwABsQEK",
+ },
+ }
+ }
+ for individualIP in UDP_IP:
+ SendCommand(message, individualIP, UDP_PORT)
+ message = {"msg": {"cmd": "status", "data": {}}}
+ time.sleep(2)
+ elif Command == "SegmentTerm":
+ message = {"msg": {"cmd": "status", "data": {}}}
+ for individualIP in UDP_IP:
+ SendCommand(message, individualIP, UDP_PORT)
+ message = {
+ "msg": {
+ "cmd": "razer",
+ "data": {
+ "pt": "uwABsQAL",
+ },
+ }
+ }
+ for individualIP in UDP_IP:
+ SendCommand(message, individualIP, UDP_PORT)
+ message = {"msg": {"cmd": "status", "data": {}}}
+ time.sleep(2)
+ elif Command == "SegmentColor":
+ gradientFlag = 1
+ color1 = np.array(color[0])
+ color2 = np.array(color[1])
+ byteArray = [187, 0, 32, 176, gradientFlag, 20]
+
+ # Interpolate between color1 and color2
+ for t in np.linspace(0, 1, 20):
+ interpolated_color = (1 - t) * color1 + t * color2
+ byteArray.extend(interpolated_color.astype(int))
+
+ num2 = 0
+ for byte in byteArray:
+ num2 ^= byte
+ byteArray.append(num2)
+ finalSendValue = base64.b64encode(bytes(byteArray)).decode()
+ message = {
+ "msg": {
+ "cmd": "razer",
+ "data": {
+ "pt": finalSendValue,
+ },
+ }
+ }
+ # elif Command == "SegmentColor":
+ # gradientFlag = 1
+ # r = color[0][0]
+ # g = color[0][1]
+ # b = color[0][2]
+ # gradientFlag = 0
+ # # byteArray = [187,0,32,176,gradientFlag,10,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b]
+ # byteArray = [
+ # 187,
+ # 0,
+ # 32,
+ # 176,
+ # gradientFlag,
+ # 10,
+ # color[0][0],
+ # color[0][1],
+ # color[0][2],
+ # color[0][0],
+ # color[0][1],
+ # color[0][2],
+ # color[0][0],
+ # color[0][1],
+ # color[0][2],
+ # color[0][0],
+ # color[0][1],
+ # color[0][2],
+ # color[0][0],
+ # color[0][1],
+ # color[0][2],
+ # color[1][0],
+ # color[1][1],
+ # color[1][2],
+ # color[1][0],
+ # color[1][1],
+ # color[1][2],
+ # color[1][0],
+ # color[1][1],
+ # color[1][2],
+ # color[1][0],
+ # color[1][1],
+ # color[1][2],
+ # color[1][0],
+ # color[1][1],
+ # color[1][2],
+ # ]
+ # num2 = 0
+ # for byte in byteArray:
+ # num2 ^= byte
+ # byteArray.append(num2)
+ # finalSendValue = base64.b64encode(bytes(byteArray)).decode()
+ # message = {
+ # "msg": {
+ # "cmd": "razer",
+ # "data": {
+ # "pt": finalSendValue,
+ # },
+ # }
+ # }
+ elif Command == "BrightLevel":
+ message = {
+ "msg": {
+ "cmd": "brightness",
+ "data": {
+ "value": brightness,
+ },
+ }
+ }
+ else:
+ raise ValueError("Bad Command: {}".format(Command))
+
+ # To print or not to print
+ if Loop == False and printStatus == True:
+ print("Govee Internal Control: {}".format(Command))
+
+ # Send command from above
+ for individualIP in UDP_IP:
+ SendCommand(message, individualIP, UDP_PORT)
+ except Exception as E:
+ errorCount += 1
+ if errorCount > 3:
+ print("Govee Internal Control Error Recursion Exit: {}".format(str(E)))
+ return
+ else:
+ print("Govee Internal Control Error: {}".format(str(E)))
+ return GoveeLocalControl(
+ Command,
+ brightness,
+ color,
+ Loop,
+ UDP_IP,
+ UDP_PORT,
+ printStatus,
+ errorCount,
+ )
+
+
+def main(DeviceIP):
+ global boxSize
+ global sock
+ errorNotified = False
+ lastColor = None
+ handle = None
+ whndl = None
+ user32 = ctypes.windll.user32
+ gdi32 = ctypes.windll.gdi32
+
+ screen_width = user32.GetSystemMetrics(0)
+ screen_height = user32.GetSystemMetrics(1)
+ print("Using Screen Dimensions: {} {}".format(screen_width, screen_height))
+ middle_x = screen_width // 2
+ middle_y = screen_height // 2
+ chunk = int(screen_width / 4)
+
+ # Get the device context for the entire screen
+ screen_dc = user32.GetDC(None)
+
+ # Create a compatible device context
+ mem_dc = gdi32.CreateCompatibleDC(screen_dc)
+
+ # Create a bitmap compatible with the screen device context
+ bmp = gdi32.CreateCompatibleBitmap(screen_dc, 1, 1)
+
+ # Select the bitmap object into the compatible device context
+ gdi32.SelectObject(mem_dc, bmp)
+
+ # Define the value of SRCCOPY manually
+ SRCCOPY = 0xCC0020
+
+ try:
+ print("Game Time Function: Turning Lights On")
+ while 1:
+ # Copy the pixel color from the screen to the bitmap
+ gdi32.BitBlt(
+ mem_dc, 0, 0, 1, 1, screen_dc, middle_x - chunk, middle_y, SRCCOPY
+ )
+
+ # Retrieve the color value of the pixel
+ pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
+
+ # Extract the individual color components (red, green, blue)
+ red = pixel_color & 0xFF
+ green = (pixel_color >> 8) & 0xFF
+ blue = (pixel_color >> 16) & 0xFF
+ pixelColor1 = (red, green, blue)
+
+ gdi32.BitBlt(
+ mem_dc, 0, 0, 1, 1, screen_dc, middle_x + chunk, middle_y, SRCCOPY
+ )
+
+ # Retrieve the color value of the pixel
+ pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
+
+ # Extract the individual color components (red, green, blue)
+ red = pixel_color & 0xFF
+ green = (pixel_color >> 8) & 0xFF
+ blue = (pixel_color >> 16) & 0xFF
+ pixelColor2 = (red, green, blue)
+ # print(str(middle_x-chunk)+"-"+str(pixelColor1))
+ # print(str(middle_x+chunk)+"-"+str(pixelColor2))
+
+ try:
+ # Send colors
+ if "Error" in pixelColor1 or "Error" in pixelColor2:
+ if errorNotified == False:
+ print("Error In Colors Loop: {}".format(pixelColor))
+ errorNotified = True
+ time.sleep(5)
+ continue
+ else:
+ # print(pixelColor)
+ lastColor = pixelColor1
+ errorNotified = False
+ GoveeLocalControl(
+ Command="SegmentColor",
+ UDP_IP=DeviceIP,
+ color=(pixelColor1, pixelColor2),
+ )
+ except Exception as E:
+ print("Govee Game Time Inner Loop Error: {}".format(str(E)))
+ # If for some reason we break from loop and we return from this thread, we want to power down lights.
+ print("Game Time Function: Turning Lights Off. Thread Is Dead.")
+ gdi32.DeleteDC(mem_dc)
+ gdi32.DeleteDC(screen_dc)
+ GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
+ raise NameError("NormalReturn")
+ except Exception as E:
+ if "NormalReturn" in str(E):
+ print("Game Time Function: Turning Lights Off. Thread Is Dead.")
+ return "NormalExit"
+ else:
+ print("Govee Game Time Loop Error: {}".format(str(E)))
+ GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
+ return "Error"
+
+
+try:
+ print("Press Ctrl + C To Quit (Lights Should Turn Off)\n\n")
+ GoveeLocalControl(Command="SegmentTerm", UDP_IP=DeviceIP)
+ GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
+ time.sleep(1)
+ GoveeLocalControl(Command="On", UDP_IP=DeviceIP)
+ GoveeLocalControl(Command="BrightLevel", UDP_IP=DeviceIP, brightness=30)
+ time.sleep(1)
+ GoveeLocalControl(Command="SegmentInit", UDP_IP=DeviceIP)
+ time.sleep(1)
+ # GoveeLocalControl(Command="SegmentColor", color=[(255, 0, 0)], UDP_IP=DeviceIP)
+ main(DeviceIP)
+except KeyboardInterrupt:
+ GoveeLocalControl(Command="SegmentTerm", UDP_IP=DeviceIP)
+ GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
+ try:
+ sys.exit(130)
+ except SystemExit:
+ os._exit(130)
diff --git a/old/GameSync2024v2.py b/old/GameSync2024v2.py
new file mode 100644
index 0000000..6e0ebf2
--- /dev/null
+++ b/old/GameSync2024v2.py
@@ -0,0 +1,138 @@
+import base64
+import time
+from enum import Enum
+from typing import List, Tuple
+
+from PIL import ImageGrab
+from pydantic import BaseModel, Field
+
+# Configuration Constants
+DEVICE_IP = ["10.1.1.54", "10.1.1.43"]
+DEVICE_IP: list[str] = ["192.168.0.108", "192.168.0.248"]
+UDP_PORT = 4003
+NUM_COLORS = 20
+
+
+# Enums for Power State
+class PowerState(Enum):
+ OFF = 0
+ ON = 1
+
+
+# Pydantic Models
+class Color(BaseModel):
+ r: int = Field(..., ge=0, le=255)
+ g: int = Field(..., ge=0, le=255)
+ b: int = Field(..., ge=0, le=255)
+
+
+class Command(BaseModel):
+ cmd: str
+ data: BaseModel
+
+
+class PowerData(BaseModel):
+ value: PowerState
+
+
+class BrightnessData(BaseModel):
+ value: int
+
+
+class ColorData(BaseModel):
+ color: Color
+ colorTemInKelvin: int = 0
+
+
+class SegmentData(BaseModel):
+ pt: str
+
+
+class PowerCommand(Command):
+ cmd: str = "turn"
+ data: PowerData
+
+
+class BrightnessCommand(Command):
+ cmd: str = "brightness"
+ data: BrightnessData
+
+
+class ColorCommand(Command):
+ cmd: str = "colorwc"
+ data: ColorData
+
+
+class SegmentCommand(Command):
+ cmd: str = "razer"
+ data: SegmentData
+
+
+# Utility Functions
+def capture_screen_and_process_colors(
+ num_devices: int,
+) -> List[List[Tuple[int, int, int]]]:
+ screen_image = ImageGrab.grab()
+ screen_width, screen_height = screen_image.size
+ vertical_positions = [
+ int(screen_height * i / NUM_COLORS) for i in range(NUM_COLORS)
+ ]
+ horizontal_sections = [
+ int(screen_width * (i + 1) / (num_devices + 1)) for i in range(num_devices)
+ ]
+ return [
+ [screen_image.getpixel((x, y)) for y in vertical_positions]
+ for x in horizontal_sections
+ ]
+
+
+def format_and_send_colors(
+ controller: LightController, colors: List[List[Tuple[int, int, int]]]
+):
+ for ip, colors_list in zip(controller.ips, colors):
+ byteArray = [187, 0, 32, 176, 1, NUM_COLORS] + [
+ c for color in colors_list for c in color
+ ]
+ checksum = sum(byteArray) % 256
+ byteArray.append(checksum)
+ encoded_data = base64.b64encode(bytes(byteArray)).decode()
+ segment_data = SegmentData(pt=encoded_data)
+ command = SegmentCommand(data=segment_data)
+ controller.send_command(command)
+
+
+def game_loop(controller: LightController):
+ try:
+ for ip in controller.ips:
+ controller.send_command(PowerCommand(data=PowerData(value=PowerState.ON)))
+ controller.send_command(BrightnessCommand(data=BrightnessData(value=100)))
+ controller.send_command(SegmentCommand(data=SegmentData(pt="uwABsQEK")))
+ time.sleep(2)
+
+ while True:
+ color_data = capture_screen_and_process_colors(len(controller.ips))
+ format_and_send_colors(controller, color_data)
+ time.sleep(1)
+ except KeyboardInterrupt:
+ print("Operation stopped by user.")
+ finally:
+ for ip in controller.ips:
+ controller.send_command(
+ ColorCommand(
+ data=ColorData(color=Color(r=255, g=165, b=0), colorTemInKelvin=0)
+ )
+ )
+ time.sleep(2)
+ for ip in controller.ips:
+ controller.send_command(SegmentCommand(data=SegmentData(pt="uwABsQAL")))
+ controller.send_command(PowerCommand(data=PowerData(value=PowerState.OFF)))
+
+
+def main():
+ controller = LightController(DEVICE_IP)
+ print("Press Ctrl+C to quit.")
+ game_loop(controller)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/old/GetGoveeDevices.py b/old/GetGoveeDevices.py
new file mode 100644
index 0000000..43384d3
--- /dev/null
+++ b/old/GetGoveeDevices.py
@@ -0,0 +1,56 @@
+import json
+import socket
+import struct
+
+
+def discover_devices(send_group, send_port, receive_port, message, timeout=5):
+ # Set up the sending socket with TTL for multicast
+ send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+ ttl = struct.pack("b", 1) # Time-to-live of the multicast message
+ send_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
+
+ # Set up the receiving socket
+ recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+ recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ recv_sock.bind(("", receive_port))
+
+ # Join multicast group
+ mreq = struct.pack("4sl", socket.inet_aton(send_group), socket.INADDR_ANY)
+ recv_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
+
+ # Send the multicast message
+ json_message = json.dumps(message)
+ print(f"Sending: {json_message}")
+ send_sock.sendto(bytes(json_message, "utf-8"), (send_group, send_port))
+ send_sock.close()
+
+ # Listen for responses
+ recv_sock.settimeout(timeout)
+ devices = []
+ try:
+ while True:
+ data, addr = recv_sock.recvfrom(10240)
+ print(f"Received response from {addr}: {data}")
+ devices.append(addr[0])
+ except socket.timeout:
+ print("Listening timeout reached. No more responses.")
+ finally:
+ recv_sock.close()
+
+ return devices
+
+
+if __name__ == "__main__":
+ send_group = "239.255.255.250"
+ send_port = 4001
+ receive_port = 4002
+ message = {
+ "msg": {
+ "cmd": "scan",
+ "data": {
+ "account_topic": "reserve",
+ },
+ }
+ }
+ devices = discover_devices(send_group, send_port, receive_port, message)
+ print("Discovered devices:", devices)
diff --git a/old/README.md b/old/README.md
new file mode 100644
index 0000000..e142368
--- /dev/null
+++ b/old/README.md
@@ -0,0 +1,45 @@
+# GoveeSync
+This code will allow you to use Govee internal UDP api to control your device as well as sync what is on the screen. You need to install the below libs before running:
+pip install wmi
+pip install pywin32
+
+# Constants
+Update the device IP of the device you want to control on line 21.
+
+# No Frills Screen Sync Script
+
+If you don't want to get into the different functions and you just want something simple you can run and have work, just run GameSync2023.py file.
+
+- Replace line 10 with your device IP, for example: DeviceIP = "192.168.0.1"
+- Open cmd prompt then simply run with - python c:\PathToFile\GameSync2023.py
+- You can simply do Ctrl + C to exit when you want to end the light sync.
+
+
+# Full Featured Code With Many Supported Commands - All of the below commands are implemented within GameSync.py
+
+To turn the device on/off:
+GoveeInternalControl("On")
+GoveeInternalControl("Off")
+
+To set the brightness:
+GoveeInternalControl("BrightLevel",100) #1-100% expressed as an integer
+GoveeInternalControl("BrightLevel",50) #1-100% expressed as an integer
+GoveeInternalControl("BrightLevel",10) #1-100% expressed as an integer
+
+To change the color:
+GoveeInternalControl("Color",color=(0,255,0)) #Color expressed as a RGB tuple
+GoveeInternalControl("Color",color=(255,0,0)) #Color expressed as a RGB tuple
+
+To go into game mode where the screen will sync to the device:
+Do note: in this mode the code will attempt to lock onto the window that is in focus.
There is a box size constant you can mess around with if you want to and see if you get better results.
This mode essentially works by creating a square in the center of the app's window. We sample a pixel at each of the four corners of the square, get the color values, then average those 4 color values together. That averaged out value is then sent to the Govee lights over UDP api.
+
+GameTime()
+
+# Testing / Searching for devices - This is optional if you want to search for available Govee LAN api devices on your network.
+Download UDPReceiver.py and UDPSender.py
+
+- Start CMD prompt, navigate to the folder where you downloaded the above files. Then type: python UDPReceiver.py
This should start a UDP multicast listener on port 4002
+- Once the listener is started open another CMD prompt. Navigate to the folder, and type: python UDPSender.py
This should output any Govee devices found to the shell.
+
+
+
diff --git a/ScreenSync.py b/old/ScreenSync.py
similarity index 99%
rename from ScreenSync.py
rename to old/ScreenSync.py
index 429d2cc..382cff6 100644
--- a/ScreenSync.py
+++ b/old/ScreenSync.py
@@ -55,6 +55,11 @@ class Message(BaseModel):
msg: Command
+class DeviceScanCommand(Command):
+ cmd: str = "scan"
+ data: dict[str, str] = {"account_topic": "reserve"}
+
+
class PowerCommand(Command):
cmd: str = "turn"
data: PowerData
diff --git a/UDPReceiver.py b/old/UDPReceiver.py
similarity index 96%
rename from UDPReceiver.py
rename to old/UDPReceiver.py
index 6d61b95..564c92d 100644
--- a/UDPReceiver.py
+++ b/old/UDPReceiver.py
@@ -1,19 +1,19 @@
-import socket
-import struct
-
-MCAST_GRP = "239.255.255.250"
-MCAST_PORT = 4002
-
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
-sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-
-sock.bind(('', MCAST_PORT))
-mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)
-
-sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
-
-while True:
- print("Listening on {}".format(MCAST_PORT))
- if sock.recv:
- print(sock.recv(10240))
+import socket
+import struct
+
+MCAST_GRP = "239.255.255.250"
+MCAST_PORT = 4002
+
+sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+sock.bind(('', MCAST_PORT))
+mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)
+
+sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
+
+while True:
+ print("Listening on {}".format(MCAST_PORT))
+ if sock.recv:
+ print(sock.recv(10240))
break
\ No newline at end of file
diff --git a/UDPSender.py b/old/UDPSender.py
similarity index 96%
rename from UDPSender.py
rename to old/UDPSender.py
index d6a237e..58179d5 100644
--- a/UDPSender.py
+++ b/old/UDPSender.py
@@ -1,24 +1,24 @@
-import json,time,socket,struct
-message = {
- "msg":{
- "cmd":"scan",
- "data":{
- "account_topic":"reserve",
- }
- }
-}
-import socket
-group = "239.255.255.250"
-port = 4001
-# 2-hop restriction in network
-ttl = 2
-sock = socket.socket(socket.AF_INET,
- socket.SOCK_DGRAM,
- socket.IPPROTO_UDP)
-sock.setsockopt(socket.IPPROTO_IP,
- socket.IP_MULTICAST_TTL,
- ttl)
-jsonResult = json.dumps(message)
-print("Sending: "+jsonResult)
-sock.sendto(bytes(jsonResult, "utf-8"), (group, port))
-
+import json,time,socket,struct
+message = {
+ "msg":{
+ "cmd":"scan",
+ "data":{
+ "account_topic":"reserve",
+ }
+ }
+}
+import socket
+group = "239.255.255.250"
+port = 4001
+# 2-hop restriction in network
+ttl = 2
+sock = socket.socket(socket.AF_INET,
+ socket.SOCK_DGRAM,
+ socket.IPPROTO_UDP)
+sock.setsockopt(socket.IPPROTO_IP,
+ socket.IP_MULTICAST_TTL,
+ ttl)
+jsonResult = json.dumps(message)
+print("Sending: "+jsonResult)
+sock.sendto(bytes(jsonResult, "utf-8"), (group, port))
+
diff --git a/old/controller.py b/old/controller.py
new file mode 100644
index 0000000..bb06ab3
--- /dev/null
+++ b/old/controller.py
@@ -0,0 +1,30 @@
+import socket
+from typing import List
+
+from models import Command, Message
+
+UDP_PORT = 4003
+
+
+# Controller Class
+class LightController:
+ def __init__(self, ips: List[str]):
+ self.ips = ips
+
+ def send_command(self, command: Command):
+ message = Message(msg=command).model_dump_json()
+
+ for ip in self.ips:
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
+ sock.sendto(message.encode(), (ip, UDP_PORT))
+ print(f"Command sent to {ip}: {message}")
+
+
+"""
+this is the model.
+Message(msg=PowerCommand(cmd='turn', data=PowerData(value=)))
+
+after model_dump_json() I get this...
+'{"msg":{"cmd":"turn","data":{}}}'
+
+"""
diff --git a/old/debug_console.png b/old/debug_console.png
new file mode 100644
index 0000000000000000000000000000000000000000..c93dbacdee1488ce5e4af440fde2eac8a96039ff
GIT binary patch
literal 106
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 SegmentData:
+ gradient_flag = 1 if gradient else 0
+ byte_array = [187, 0, 32, 176, gradient_flag, len(list_of_colors)]
+
+ for color in list_of_colors:
+ byte_array.extend(color.rgb)
+
+ num2 = 0
+ for byte in byte_array:
+ num2 ^= byte
+ byte_array.append(num2)
+ final_send_value = base64.b64encode(bytes(byte_array)).decode()
+ return SegmentData(pt=final_send_value)
+
+
+def get_column_indices(column_number: int):
+ """
+ screen example for MAX_LED_COLOR_GRADIENT = 10
+
+ column numbers
+ 0 1 2 3 4 5 6 7 8 9
+ 0 x x x x x x x x x x
+ 1 x x x x x x x x x x
+ 2 x x x x x x x x x x
+ 3 x x x x x x x x x x
+ 4 x x x x x x x x x x
+ 5 x x x x x x x x x x
+ 6 x x x x x x x x x x
+ 7 x x x x x x x x x x
+ 8 x x x x x x x x x x
+ 9 x x x x x x x x x x
+
+ My device has index 0 at the bottom, so I need to reverse the row indices.
+ """
+ column_indices = [column_number] * MAX_LED_COLOR_GRADIENT
+ row_indices = list(range(MAX_LED_COLOR_GRADIENT))[::-1]
+ screen_indices = [column_indices, row_indices]
+ return screen_indices
+
+
+def example_usage():
+ default_color = Color(r=255, g=165, b=0)
+ # controller = LightController(["192.168.0.108", "192.168.0.248"])
+ left_column_screen_indices = [(0, i) for i in range(MAX_LED_COLOR_GRADIENT)]
+ left_column_device = GoveeLightDevice(
+ "192.168.0.248", "Govee Light Left", left_column_screen_indices
+ )
+
+ try:
+ # Example usage of the PowerCommand model
+ left_column_device.power_on()
+
+ # Example usage of the ColorCommand model
+ left_column_device.set_color(default_color)
+ time.sleep(1)
+
+ # Example usage of the BrightnessCommand model
+ left_column_device.set_brightness(50)
+ time.sleep(1)
+ left_column_device.set_brightness(100)
+ time.sleep(1)
+
+ # Example usage of the SegmentCommand model
+ left_column_device.initialize_segment()
+
+ left_column_device.set_segment_colors(
+ [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0)],
+ )
+ time.sleep(3)
+ left_column_device.set_segment_colors(
+ [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0), Color(r=0, g=0, b=25)],
+ )
+ time.sleep(3)
+
+ left_column_device.terminate_segment()
+
+ except Exception as e:
+ print(e)
+ finally:
+ left_column_device.power_off()
+
+
+def capture_screen_and_process_colors() -> np.ndarray:
+ screen_image = ImageGrab.grab()
+ resized_image = screen_image.resize(
+ (MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT)
+ )
+
+ # Increase the saturation
+ converter = ImageEnhance.Color(resized_image)
+ saturated_image = converter.enhance(1.5)
+
+ color_matrix = np.array(saturated_image).transpose(1, 0, 2)
+
+ # Replace colors that are too dark
+ color_sums = color_matrix.sum(axis=2)
+ dark_colors = color_sums < 10
+ color_matrix[dark_colors] = [10, 10, 10]
+
+ return color_matrix
+
+
+def game_loop(devices: list[GoveeLightDevice]):
+ try:
+ for device in devices:
+ device.power_on()
+ time.sleep(0.1)
+ device.set_brightness(100)
+ time.sleep(0.1)
+ device.set_color(Color(r=55, g=165, b=0))
+
+ time.sleep(0.5)
+
+ for device in devices:
+ device.initialize_segment()
+
+ while True:
+ screen_colors = capture_screen_and_process_colors()
+ for device in devices:
+ color_data = [
+ Color(r=r, g=g, b=b)
+ for r, g, b in screen_colors[*device.screen_positions]
+ ]
+ device.set_segment_colors(color_data)
+ time.sleep(0.005) # 200 FPS
+ except KeyboardInterrupt:
+ print("Operation stopped by user.")
+ finally:
+ for device in devices:
+ device.terminate_segment()
+ time.sleep(0.5)
+ device.power_off()
+
+
+def run():
+ left_column_device = GoveeLightDevice(
+ "192.168.0.248", "Govee Light Left", get_column_indices(0)
+ )
+ right_column_device = GoveeLightDevice(
+ "192.168.0.108",
+ "Govee Light Right",
+ get_column_indices(MAX_LED_COLOR_GRADIENT - 1),
+ )
+ devices = [left_column_device, right_column_device]
+
+ def power_off_devices_and_exit():
+ if left_column_device is not None:
+ left_column_device.power_off()
+ if right_column_device is not None:
+ right_column_device.power_off()
+ exit()
+
+ print("Starting Govee Light Controller...")
+ signal.signal(signal.SIGTERM, lambda signum, frame: power_off_devices_and_exit())
+ signal.signal(signal.SIGINT, lambda signum, frame: power_off_devices_and_exit())
+ game_loop(devices=devices)
+
+
+if __name__ == "__main__":
+ run()
diff --git a/old/stuff.md b/old/stuff.md
new file mode 100644
index 0000000..6dd9b57
--- /dev/null
+++ b/old/stuff.md
@@ -0,0 +1,12 @@
+Listening on 4002
+b'{"msg":{"cmd":"scan","data":{"ip":"192.168.0.248","device":"53:47:C7:32:34:35:56:8D","sku":"H6076","bleVersionHard":"3.01.01","bleVersionSoft":"1.04.06","wifiVersionHard":"1.00.10","wifiVersionSoft":"1.02.11"}}}'
+
+
+
+(openapi) C:\Users\kaaik\Documents\GitHub\GoveeSync>python UDPReceiver.py
+Listening on 4002
+b'{"msg":{"cmd":"scan","data":{"ip":"192.168.0.108","device":"03:F0:C5:32:34:35:1A:57","sku":"H6076","bleVersionHard":"3.01.01","bleVersionSoft":"1.04.06","wifiVersionHard":"1.00.10","wifiVersionSoft":"1.02.11"}}}'
+
+(openapi) C:\Users\kaaik\Documents\GitHub\GoveeSync>python UDPReceiver.py
+Listening on 4002
+b'{"msg":{"cmd":"scan","data":{"ip":"192.168.0.248","device":"53:47:C7:32:34:35:56:8D","sku":"H6076","bleVersionHard":"3.01.01","bleVersionSoft":"1.04.06","wifiVersionHard":"1.00.10","wifiVersionSoft":"1.02.11"}}}'
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..44a8a0b
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,295 @@
+# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.6.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
+ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.4"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
+ {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"},
+ {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"},
+ {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"},
+ {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"},
+ {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"},
+ {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"},
+ {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"},
+ {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"},
+ {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"},
+ {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"},
+ {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"},
+ {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"},
+ {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"},
+ {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"},
+ {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"},
+ {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"},
+ {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"},
+ {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"},
+ {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"},
+ {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"},
+ {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
+ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
+]
+
+[[package]]
+name = "pillow"
+version = "10.3.0"
+description = "Python Imaging Library (Fork)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
+ {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
+ {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
+ {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
+ {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
+ {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
+ {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
+ {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
+ {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
+ {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
+ {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
+ {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
+ {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
+ {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
+ {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
+ {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
+ {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
+ {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
+ {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
+ {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
+ {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
+ {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
+ {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
+ {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
+ {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
+ {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
+ {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
+ {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
+ {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
+ {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
+ {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
+ {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
+ {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
+ {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
+ {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
+ {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
+ {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
+ {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
+ {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
+ {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
+ {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
+ {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
+fpx = ["olefile"]
+mic = ["olefile"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+typing = ["typing-extensions"]
+xmp = ["defusedxml"]
+
+[[package]]
+name = "pydantic"
+version = "2.7.0"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"},
+ {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.18.1"
+typing-extensions = ">=4.6.1"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.18.1"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"},
+ {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"},
+ {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"},
+ {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"},
+ {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"},
+ {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"},
+ {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"},
+ {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"},
+ {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"},
+ {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"},
+ {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"},
+ {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"},
+ {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"},
+ {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"},
+ {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"},
+ {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"},
+ {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"},
+ {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"},
+ {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"},
+ {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"},
+ {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "ruff"
+version = "0.3.7"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
+ {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
+ {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
+ {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
+ {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
+ {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
+ {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
+ {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
+ {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
+ {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
+ {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
+ {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
+ {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
+ {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
+ {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
+ {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
+ {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.11.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.12"
+content-hash = "16ac7da567fa2620d9ccff783306fa96f8041740a0d0c263d5b6b8174bec8b2a"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..bd29638
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,71 @@
+[tool.poetry]
+name = "govee-screen-sync"
+version = "0.0.1"
+description = "Sync your Govee device to the pixels on your screen"
+authors = ["Steven Kauwe "]
+
+[tool.poetry.dependencies]
+python = "^3.12"
+numpy = "^1.26.4"
+pillow = "^10.3.0"
+pydantic = "^2.4.2"
+
+[tool.poetry.dev-dependencies]
+ruff = "^0.3.7"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.ruff]
+# Exclude a variety of commonly ignored directories.
+exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "build",
+ "dist",
+ "node_modules",
+ "venv",
+]
+extend-select = ["I", "Q"]
+ignore-init-module-imports = true
+# Same as Black.
+line-length = 99
+indent-width = 4
+# Assume Python 3.12
+target-version = "py312"
+
+[tool.ruff.lint]
+# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
+select = ["E4", "E7", "E9", "F"]
+ignore = []
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.ruff.format]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
\ No newline at end of file
diff --git a/src/govee_screen_sync/__init__.py b/src/govee_screen_sync/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/govee_screen_sync/__pycache__/__init__.cpython-312.pyc b/src/govee_screen_sync/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e4c9476968b86c96b60d380f5fc82e41667fb37b
GIT binary patch
literal 168
zcmX@j%ge<81Y#eg(?IlN5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a(A|h2`x@7Dvrrc
zOw7!Vami0E%}vcKDUNZ^Eb%B!igC{`OHB=~%u9|*2eIOdlZ#SQ^Wuv^BJuH=d6^~g
l@p=W7zc_4i^HWN5QtgUZf#xy-aWRPTk(rT^v4|PS0s!L`572q
zg7o`oGT&l#adirHzr_*i5)dEY9~5#+%-1m@-pAD?-r3*BKPcWk$kD~q)h}cv!)K7W
zzg(TIVnT~ki;82i6B9GDV_fo+OLJ56N{VCLGfO;5lVaTS%TiN=EAx_L(m|~F;^d;#
z)V%m&kVtZVURq|lUP0wA4x8Nkl+v73yCP1YsURm8ivo!c%#4hTH#m43SZ{FiH*nmL
Nl)lIyR>TgJ0RWjwLMs3O
literal 0
HcmV?d00001
diff --git a/src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc b/src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6978e49bf92ccbf34b4ae62cbad419b1e7bda48
GIT binary patch
literal 3877
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
Sc{h&10k8lY*DE>Fz8L&|R
literal 0
HcmV?d00001
diff --git a/src/govee_screen_sync/config.py b/src/govee_screen_sync/config.py
new file mode 100644
index 0000000..770ed96
--- /dev/null
+++ b/src/govee_screen_sync/config.py
@@ -0,0 +1,3 @@
+DEBUG = False
+UDP_PORT = 4003
+MAX_LED_COLOR_GRADIENT = 10
diff --git a/src/govee_screen_sync/game_sync.py b/src/govee_screen_sync/game_sync.py
new file mode 100644
index 0000000..a9fca0b
--- /dev/null
+++ b/src/govee_screen_sync/game_sync.py
@@ -0,0 +1,64 @@
+import time
+
+from govee_screen_sync.light_device import GoveeLightDevice
+from govee_screen_sync.models import Color
+from govee_screen_sync.screen_capture import capture_screen_and_process_colors
+
+
+class FrameCounter:
+ def __init__(self):
+ self.last_100_fps = []
+ self.start_time = time.time()
+ self.previous_time = time.time()
+
+ def update_and_print(self):
+ elapsed_time = time.time() - self.previous_time
+ self.previous_time = time.time()
+ time_since_start = time.time() - self.start_time
+ fps = 1 / elapsed_time
+ self.last_100_fps = self.last_100_fps[-99:]
+ self.last_100_fps.append(fps)
+ average_fps = (
+ sum(self.last_100_fps) / len(self.last_100_fps) if self.last_100_fps else 0
+ )
+ print(
+ f"\rFPS: {fps:.1f} - Average FPS {average_fps:.1f} - Total Time:{time_since_start:.1f}",
+ end="",
+ )
+
+ return fps
+
+ def reset(self):
+ self.frame_count = 0
+ self.start_time = time.time()
+
+
+def game_loop(devices: list[GoveeLightDevice]):
+ frame_counter = FrameCounter()
+ try:
+ for device in devices:
+ device.power_on()
+ device.set_brightness(100)
+
+ for device in devices:
+ device.initialize_segment()
+
+ while True:
+ screen_colors = capture_screen_and_process_colors()
+ for device in devices:
+ selected_rows = screen_colors[device.screen_positions[0], :, :]
+ selected_columns = selected_rows[:, device.screen_positions[1], :]
+ color_data = [
+ Color(r=r, g=g, b=b)
+ for r, g, b in selected_columns.mean(axis=1).astype(int)
+ ]
+ device.set_segment_colors(color_data)
+ frame_counter.update_and_print()
+
+ except KeyboardInterrupt:
+ print("Operation stopped by user.")
+ finally:
+ for device in devices:
+ device.terminate_segment()
+ device.set_color(Color())
+ device.power_off()
diff --git a/src/govee_screen_sync/light_device.py b/src/govee_screen_sync/light_device.py
new file mode 100644
index 0000000..eacba7a
--- /dev/null
+++ b/src/govee_screen_sync/light_device.py
@@ -0,0 +1,132 @@
+import base64
+import socket
+import time
+
+from govee_screen_sync.config import DEBUG, MAX_LED_COLOR_GRADIENT, UDP_PORT
+from govee_screen_sync.models import (
+ BrightnessCommand,
+ BrightnessData,
+ Color,
+ ColorCommand,
+ ColorData,
+ Command,
+ Message,
+ PowerCommand,
+ PowerData,
+ PowerState,
+ SegmentCommand,
+ SegmentData,
+)
+
+
+class GoveeLightDevice:
+ def __init__(self, ip: str, name: str, screen_positions: list[tuple[int, int]]):
+ self.ip = ip
+ self.name = name
+ self.screen_positions = screen_positions
+
+ def _send_command(self, command: Command, sleep_time=0.1):
+ message = Message(msg=command).model_dump_json()
+
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
+ sock.sendto(message.encode(), (self.ip, UDP_PORT))
+ if DEBUG:
+ print(f"Command sent to {self.ip}: {message}")
+ time.sleep(sleep_time)
+
+ def power_on(self):
+ power_on_command = PowerCommand(data=PowerData(value=PowerState.ON))
+ self._send_command(power_on_command)
+
+ def power_off(self):
+ power_off_command = PowerCommand(data=PowerData(value=PowerState.OFF))
+ self._send_command(power_off_command)
+
+ def set_color(self, color: Color):
+ color_command = ColorCommand(data=ColorData(color=color))
+ self._send_command(color_command)
+
+ def set_brightness(self, brightness: int):
+ brightness_command = BrightnessCommand(data=BrightnessData(value=brightness))
+ self._send_command(brightness_command)
+
+ def initialize_segment(self):
+ """Initializes a segment to be set with set_segment_colors."""
+ segment_command = SegmentCommand(data=SegmentData(pt="uwABsQEK"))
+ self._send_command(segment_command)
+
+ def terminate_segment(self):
+ """Returns the color to the state before the segment was initialized."""
+ segment_command = SegmentCommand(data=SegmentData(pt="uwABsQAL"))
+ self._send_command(segment_command)
+
+ def set_segment_colors(self, list_of_colors: list[Color], gradient=True):
+ """Sets the colors of the segment to the given list of colors.
+ color list of size (min 2, max 10)
+
+ probably move "intrepolation" and "resolution" to some other place.
+
+ colors ordering goes from bottom to top for a lamp.
+ not sure about a strip.
+ but probably power source to the end of the strip.
+ """
+
+ assert (
+ 2 <= len(list_of_colors) <= MAX_LED_COLOR_GRADIENT
+ ), f"Color list must be between 1 and 10, got {len(list_of_colors)}"
+
+ segment_color_data = self._get_segment_color_data(list_of_colors, gradient)
+ segment_command = SegmentCommand(data=segment_color_data)
+ self._send_command(segment_command, sleep_time=0)
+
+ def _get_segment_color_data(
+ self, list_of_colors: list[Color], gradient=True
+ ) -> SegmentData:
+ """Returns the SegmentData object for the given list of colors.
+
+ The segment data is a base64 encoded string that represents the colors to be set.
+ I have no idea what the values for the first 4 bytes are.
+ I assume the 6th byte is the number of colors.
+ """
+ gradient_flag = 1 if gradient else 0
+ byte_array = [187, 0, 32, 176, gradient_flag, len(list_of_colors)]
+
+ for color in list_of_colors:
+ byte_array.extend(color.rgb)
+
+ num2 = 0
+ for byte in byte_array:
+ num2 ^= byte
+ byte_array.append(num2)
+ final_send_value = base64.b64encode(bytes(byte_array)).decode()
+ return SegmentData(pt=final_send_value)
+
+
+def get_device_location_indices(
+ column_indices: list[int] | None = None, row_indices: list[int] | None = None
+):
+ """
+ screen example for MAX_LED_COLOR_GRADIENT = 10
+
+ column numbers
+ 0 1 2 3 4 5 6 7 8 9
+ 0 x x x x x x x x x x
+ 1 x x x x x x x x x x
+ 2 x x x x x x x x x x
+ 3 x x x x x x x x x x
+ 4 x x x x x x x x x x
+ 5 x x x x x x x x x x
+ 6 x x x x x x x x x x
+ 7 x x x x x x x x x x
+ 8 x x x x x x x x x x
+ 9 x x x x x x x x x x
+
+ My device has index 0 at the bottom, so I need to reverse the row indices.
+ """
+ if column_indices is None:
+ column_indices = list(range(MAX_LED_COLOR_GRADIENT))
+ if row_indices is None:
+ # light lamp colors are ordered from bottom to top
+ row_indices = list(range(MAX_LED_COLOR_GRADIENT)[::-1])
+ screen_indices = [row_indices, column_indices]
+ return screen_indices
diff --git a/src/govee_screen_sync/main.py b/src/govee_screen_sync/main.py
new file mode 100644
index 0000000..480c2b3
--- /dev/null
+++ b/src/govee_screen_sync/main.py
@@ -0,0 +1,49 @@
+import signal
+
+from govee_screen_sync.game_sync import game_loop
+from govee_screen_sync.light_device import GoveeLightDevice, get_device_location_indices
+from govee_screen_sync.utils import color_device_by_ip, discover_devices
+
+
+def get_my_devices() -> list[GoveeLightDevice] | None:
+ """Returns a list of GoveeLightDevice objects that represent the devices in the room.
+ This function should be implemented by the user.
+
+ If you don't know the IP addresses of your devices, return an empty list
+ """
+ left_column_device = GoveeLightDevice(
+ "192.168.0.248", "Govee Light Right", get_device_location_indices([7, 8, 9])
+ )
+ right_column_device = GoveeLightDevice(
+ "192.168.0.108",
+ "Govee Light Left",
+ get_device_location_indices([0, 1, 2]),
+ )
+
+ my_devices = [left_column_device, right_column_device]
+ print(f"Devices: {[my_devices.name for my_devices in my_devices]}")
+
+ return my_devices
+ # return []
+
+
+def run():
+ devices = get_my_devices()
+ if not devices:
+ color_device_by_ip()
+ return
+ discover_devices(expected_devices=devices)
+
+ def power_off_devices_and_exit():
+ for device in devices:
+ device.power_off()
+ exit()
+
+ print("Starting Govee Light Controller...")
+ signal.signal(signal.SIGTERM, lambda signum, frame: power_off_devices_and_exit())
+ signal.signal(signal.SIGINT, lambda signum, frame: power_off_devices_and_exit())
+ game_loop(devices=devices)
+
+
+if __name__ == "__main__":
+ run()
diff --git a/src/govee_screen_sync/models.py b/src/govee_screen_sync/models.py
new file mode 100644
index 0000000..9a39b2e
--- /dev/null
+++ b/src/govee_screen_sync/models.py
@@ -0,0 +1,82 @@
+from enum import Enum
+from typing import Union
+
+from pydantic import BaseModel, Field
+
+
+class PowerState(Enum):
+ OFF = 0
+ ON = 1
+
+
+class Color(BaseModel):
+ r: int = Field(default=145, ge=0, le=255)
+ g: int = Field(default=125, ge=0, le=255)
+ b: int = Field(default=0, ge=0, le=255)
+
+ @property
+ def rgb(self):
+ return (self.r, self.g, self.b)
+
+ @classmethod
+ def from_rgb(cls, rgb: tuple[int, int, int]):
+ return cls(r=rgb[0], g=rgb[1], b=rgb[2])
+
+
+class PowerData(BaseModel):
+ value: PowerState
+
+
+class BrightnessData(BaseModel):
+ value: int
+
+
+class ColorData(BaseModel):
+ color: Color
+ colorTemInKelvin: int = 0
+
+
+class SegmentData(BaseModel):
+ pt: str
+
+
+class DeviceScanData(BaseModel):
+ account_topic: str = "reserve"
+
+
+class Command(BaseModel):
+ cmd: str
+ data: Union[PowerData, BrightnessData, ColorData, SegmentData]
+
+
+class Message(BaseModel):
+ msg: Command
+
+
+class PowerCommand(Command):
+ cmd: str = "turn"
+ data: PowerData
+
+
+class BrightnessCommand(Command):
+ cmd: str = "brightness"
+ data: BrightnessData
+
+
+class ColorCommand(Command):
+ cmd: str = "colorwc"
+ data: ColorData
+
+
+class SegmentCommand(Command):
+ cmd: str = "razer"
+ data: SegmentData
+
+
+class DeviceScanCommand(Command):
+ cmd: str = "scan"
+ data: DeviceScanData = DeviceScanData()
+
+
+class DeviceScanMessage(Message):
+ msg: DeviceScanCommand = DeviceScanCommand()
diff --git a/src/govee_screen_sync/screen_capture.py b/src/govee_screen_sync/screen_capture.py
new file mode 100644
index 0000000..f42b8ee
--- /dev/null
+++ b/src/govee_screen_sync/screen_capture.py
@@ -0,0 +1,79 @@
+import numpy as np
+from PIL import Image, ImageEnhance, ImageGrab
+
+from govee_screen_sync.config import DEBUG, MAX_LED_COLOR_GRADIENT
+
+
+def capture_screen_and_process_colors() -> np.ndarray:
+ # Capture the entire screen
+ screen_image = ImageGrab.grab()
+ resize_factor = 10
+
+ # Resize image to a manageable size that maintains the aspect ratio of 10x10 blocks
+ resized_image = screen_image.resize(
+ (MAX_LED_COLOR_GRADIENT * resize_factor, MAX_LED_COLOR_GRADIENT * resize_factor)
+ )
+
+ # Increase the saturation by 1.5 times
+ converter = ImageEnhance.Color(resized_image)
+ saturated_image = converter.enhance(2.5)
+
+ # Convert image to numpy array for processing
+ color_matrix = np.array(saturated_image)
+
+ # Reshape the array to (10, 10, 10, 10, 3) where each 10x10 block's pixels are separately accessible
+ color_matrix = color_matrix.reshape(
+ (
+ MAX_LED_COLOR_GRADIENT,
+ resize_factor,
+ MAX_LED_COLOR_GRADIENT,
+ resize_factor,
+ 3,
+ )
+ )
+
+ # I favor red > green > blue for the most "colorful pixel". Let's scale the colors accordingly
+ biased_color_matrix = color_matrix.copy()
+ biased_color_matrix[..., 0] *= 2
+ biased_color_matrix[..., 1] *= 1
+ biased_color_matrix[..., 2] *= 1
+
+ # Compute variance across each pixel to find the most "colorful" pixel
+ pixel_variances = np.var(biased_color_matrix, axis=4)
+
+ # Transpose the array so that the 10x10 blocks are along the last two axes
+ pixel_variances_transposed = pixel_variances.transpose(0, 2, 1, 3)
+
+ # Flatten the last two dimensions
+ pixel_variances_flattened = pixel_variances_transposed.reshape(
+ MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT, -1
+ )
+
+ # Compute argmax over the flattened dimension
+ max_colorful_indices = np.unravel_index(
+ pixel_variances_flattened.argmax(axis=2), (resize_factor, resize_factor)
+ )
+
+ # For each block, get the most colorful pixel
+ most_colorful_pixels = color_matrix[
+ np.arange(MAX_LED_COLOR_GRADIENT)[:, None],
+ max_colorful_indices[0],
+ np.arange(MAX_LED_COLOR_GRADIENT),
+ max_colorful_indices[1],
+ ]
+
+ most_colorful_pixels = most_colorful_pixels.reshape(
+ MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT, 3
+ )
+
+ if DEBUG:
+ saturated_image.save("debug_saturated_image.png")
+
+ most_colorful_pixels_image = Image.fromarray(
+ most_colorful_pixels.astype(np.uint8), "RGB"
+ )
+ most_colorful_pixels_image.save("debug_preview.png")
+ # raise an exception to stop the program and show the image
+ raise Exception("Debug mode enabled. Stopping program to show image.")
+
+ return most_colorful_pixels
diff --git a/src/govee_screen_sync/utils.py b/src/govee_screen_sync/utils.py
new file mode 100644
index 0000000..aaff615
--- /dev/null
+++ b/src/govee_screen_sync/utils.py
@@ -0,0 +1,109 @@
+import socket
+import struct
+import time
+
+from govee_screen_sync.light_device import GoveeLightDevice
+from govee_screen_sync.models import Color, DeviceScanMessage
+
+
+def discover_devices(
+ send_group: str = "239.255.255.250",
+ send_port: int = 4001,
+ receive_port: int = 4002,
+ message: DeviceScanMessage = DeviceScanMessage(),
+ timeout: int = 2,
+ expected_devices: list[GoveeLightDevice] = None,
+):
+ # Set up the sending socket with TTL for multicast
+ send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+ ttl = struct.pack("b", 1) # Time-to-live of the multicast message
+ send_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
+
+ # Set up the receiving socket
+ recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+ recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ recv_sock.bind(("", receive_port))
+
+ # Join multicast group
+ mreq = struct.pack("4sl", socket.inet_aton(send_group), socket.INADDR_ANY)
+ recv_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
+
+ # Send the multicast message
+ json_message = message.model_dump_json()
+ print(f"Sending: {json_message}")
+ send_sock.sendto(bytes(json_message, "utf-8"), (send_group, send_port))
+ send_sock.close()
+
+ # Listen for responses
+ recv_sock.settimeout(timeout)
+ device_ips = []
+ ti = time.time()
+ try:
+ while True:
+ data, addr = recv_sock.recvfrom(10240)
+ print(
+ f"Received response from {addr}: {data} in {time.time() - ti} seconds."
+ )
+ device_ips.append(addr[0])
+
+ # Check if all expected devices have responded
+ if expected_devices and all(
+ device.ip in device_ips for device in expected_devices
+ ):
+ print("All expected devices have responded.")
+ break
+ except socket.timeout:
+ print("Listening timeout reached. No more responses.")
+ finally:
+ recv_sock.close()
+ return device_ips
+
+
+def color_device_by_ip():
+ # For each discovered device, create a GoveeLightDevice object
+ devices = [
+ GoveeLightDevice(ip, f"Govee Light {i}", [])
+ for i, ip in enumerate(discover_devices())
+ ]
+
+ rainbow_rgb = {
+ "red": (255, 0, 0),
+ "orange": (255, 165, 0),
+ "yellow": (255, 255, 0),
+ "green": (0, 128, 0),
+ "blue": (0, 0, 255),
+ "indigo": (75, 0, 130),
+ "violet": (238, 130, 238),
+ }
+
+ # Light up the devices to indicate they have been discovered
+ device_count_location = 0
+ for i, device in enumerate(devices):
+ device.power_on()
+ device.set_brightness(100)
+ device.initialize_segment()
+
+ color_index = i % len(rainbow_rgb)
+ if i % 2 != 0:
+ color_index = len(rainbow_rgb) - color_index - 1
+ color_name = list(rainbow_rgb.keys())[color_index]
+ color_rgb = rainbow_rgb[color_name]
+
+ segment_colors = [Color.from_rgb(color_rgb) for _ in range(10)]
+ if i % len(rainbow_rgb) == 0 and i != 0:
+ device_count_location += 1
+
+ segment_colors[device_count_location] = Color.from_rgb((5, 5, 5))
+ device.set_segment_colors(segment_colors, gradient=False)
+
+ print(
+ f"Device at ip {device.ip} as color {color_name} in location {device_count_location}"
+ )
+
+ _ = input("Press enter to power off devices...")
+
+ for device in devices:
+ device.terminate_segment()
+ device.power_off()
+
+ return devices
From 4d1ba9a59146f39849085fbc12f5959a74b815c4 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:36:50 -0400
Subject: [PATCH 06/11] delete old code
---
old/BasicControls.py | 22 --
old/GameSync.py | 189 ----------------
old/GameSync2023.py | 179 ---------------
old/GameSync2024.py | 335 ----------------------------
old/GameSync2024v2.py | 138 ------------
old/GetGoveeDevices.py | 56 -----
old/README.md | 45 ----
old/ScreenSync.py | 401 ----------------------------------
old/UDPReceiver.py | 19 --
old/UDPSender.py | 24 --
old/controller.py | 30 ---
old/debug_console.png | Bin 106 -> 0 bytes
old/debug_preview.png | Bin 352 -> 0 bytes
old/debug_saturated_image.png | Bin 9073 -> 0 bytes
old/models.py | 298 -------------------------
old/stuff.md | 12 -
16 files changed, 1748 deletions(-)
delete mode 100644 old/BasicControls.py
delete mode 100644 old/GameSync.py
delete mode 100644 old/GameSync2023.py
delete mode 100644 old/GameSync2024.py
delete mode 100644 old/GameSync2024v2.py
delete mode 100644 old/GetGoveeDevices.py
delete mode 100644 old/README.md
delete mode 100644 old/ScreenSync.py
delete mode 100644 old/UDPReceiver.py
delete mode 100644 old/UDPSender.py
delete mode 100644 old/controller.py
delete mode 100644 old/debug_console.png
delete mode 100644 old/debug_preview.png
delete mode 100644 old/debug_saturated_image.png
delete mode 100644 old/models.py
delete mode 100644 old/stuff.md
diff --git a/old/BasicControls.py b/old/BasicControls.py
deleted file mode 100644
index ffe45ce..0000000
--- a/old/BasicControls.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from controller import LightController
-from models import Color, ColorCommand, ColorData, PowerCommand, PowerData, PowerState
-
-DeviceIP = ["192.168.0.108", "192.168.0.248"]
-
-
-def main():
- controller = LightController(["192.168.0.108", "192.168.0.248"])
- # Example usage of the PowerCommand model
- power_on_command = PowerCommand(data=PowerData(value=PowerState.ON))
- controller.send_command(power_on_command)
-
- # Example usage of the ColorCommand model
- color_command = ColorCommand(data=ColorData(color=Color(r=255, g=124, b=0)))
- controller.send_command(color_command)
-
- power_on_command = PowerCommand(data=PowerData(value=PowerState.OFF))
- controller.send_command(power_on_command)
-
-
-if __name__ == "__main__":
- main()
diff --git a/old/GameSync.py b/old/GameSync.py
deleted file mode 100644
index ccdc471..0000000
--- a/old/GameSync.py
+++ /dev/null
@@ -1,189 +0,0 @@
-# pywin32 must be installed
-# pip install wmi
-
-import win32gui
-import win32process
-import win32pdhutil
-import wmi
-import time
-import socket
-import json
-import math
-import threading
-from ctypes import *
-import ctypes.wintypes
-
-#Globals, you should not change these, leave them as is.
-appChange = True
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-
-#Constants, you should change these.
-GoveeDeviceIP = '10.1.1.43'
-boxSize = 50
-
-
-def GoveeInternalControl(Command,brightness=0,color=None,Loop=False,UDP_IP=GoveeDeviceIP,UDP_PORT=4003):
- lastColor = None
- global sock
- global stopLoop
-
- def SendCommand(message,UDP_IP,UDP_PORT):
- jsonResult = json.dumps(message)
- # print("Sending: {}".format(Command))
- sock.sendto(bytes(jsonResult, "utf-8"), (UDP_IP, UDP_PORT))
-
- try:
- if Command == "On":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":1,
- }
- }
- }
- elif Command == "Off":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":0,
- }
- }
- }
- elif Command == "Color":
- message = {
- "msg":{
- "cmd":"colorwc",
- "data":{
- "color":{"r":color[0],"g":color[1],"b":color[2]},
- "colorTemInKelvin":0
- }
- }
- }
- elif Command == "BrightLevel":
- message = {
- "msg":{
- "cmd":"brightness",
- "data":{
- "value":brightness,
- }
- }
- }
- else:
- raise ValueError("Bad Command: {}".format(Command))
-
- if Loop == False:
- print("\nGovee Internal Control: {}".format(Command))
-
- SendCommand(message,UDP_IP,UDP_PORT)
- except Exception as E:
- print("\nGovee Internal Control Error: {}".format(str(E)))
-
-def DetectFocusChange():
- global appChange
- currentPID = ""
- while(1):
- time.sleep(1.5)
- whndl = win32gui.GetForegroundWindow()
- tempPID = (win32process.GetWindowThreadProcessId(whndl))[1]
- if currentPID != tempPID:
- print("\nDetected Window Change: {}".format(tempPID))
- currentPID = tempPID
- appChange = True
-
-def GameTime():
- global appChange
- global boxSize
- lastColor = None
- pid = ""
-
- def GetColors(handle,upperLeft,lowerLeft,upperRight,lowerRight):
- try:
- def rgbint2rgbtuple(RGBint,skew,):
- red = RGBint & 255
- green = (RGBint >> 8) & 255
- blue = (RGBint >> 16) & 255
- return (red * skew,green * skew,blue *skew)
-
- pixels = [rgbint2rgbtuple(win32gui.GetPixel(handle,upperLeft[0],upperLeft[1]),1),
- rgbint2rgbtuple(win32gui.GetPixel(handle,lowerLeft[0],lowerLeft[1]),1),
- rgbint2rgbtuple(win32gui.GetPixel(handle,upperRight[0],upperRight[1]),1),
- rgbint2rgbtuple(win32gui.GetPixel(handle,lowerRight[0],lowerRight[1]),1)]
- red = 0
- green = 0
- blue = 0
- for pixel in pixels:
- red += pixel[0]**2
- green += pixel[1]**2
- blue += pixel[2]**2
- return int(math.sqrt(red/len(pixels))),int(math.sqrt(green/len(pixels))),int(math.sqrt(blue/len(pixels)))
- except Exception as E:
- # time.sleep(10)
- return "Get Colors Error: {}".format(str(E))
-
- try:
- GoveeInternalControl("On")
- GoveeInternalControl("BrightLevel",brightness=100)
- while(1):
- if appChange == True:
- handle = None
- whndl = None
- whndl = win32gui.GetForegroundWindow()
- pid =(win32process.GetWindowThreadProcessId(whndl))[1]
- handle = win32gui.GetWindowDC(whndl)
- txt = win32gui.GetWindowText(whndl)
- rect = ctypes.wintypes.RECT()
- DWMWA_EXTENDED_FRAME_BOUNDS = 9
- ctypes.windll.dwmapi.DwmGetWindowAttribute(
- ctypes.wintypes.HWND(whndl),
- ctypes.wintypes.DWORD(DWMWA_EXTENDED_FRAME_BOUNDS),
- ctypes.byref(rect),
- ctypes.sizeof(rect)
- )
- size = (rect.right - rect.left, rect.bottom - rect.top)
- windowWidth = size[0]
- windowHeight = size[1]
- centerWidth = int(windowWidth / 2)
- centerHeight = int(windowHeight / 2)
- upperLeft = (centerWidth-boxSize,centerHeight-boxSize)
- lowerLeft = (centerWidth-boxSize,centerHeight+boxSize)
- upperRight = (centerWidth+boxSize,centerHeight-boxSize)
- lowerRight = (centerWidth+boxSize,centerHeight+boxSize)
- print("\nFound New Focused Window:\nPid: {}\nWidth: {}\nHeight: {}\nupperLeft: {}\nlowerLeft: {}\nupperRight: {}\nlowerRight: {}".format(pid,windowWidth,windowHeight,upperLeft,lowerLeft,upperRight,lowerRight))
- appChange = False
-
- pixelColor = GetColors(handle,upperLeft,lowerLeft,upperRight,lowerRight)
- if pixelColor == lastColor:
- continue
- elif "Error" in pixelColor:
- print("\nError In Loop: {}".format(pixelColor))
- else:
- lastColor = pixelColor
- GoveeInternalControl("Color",color=pixelColor,Loop=True)
- GoveeInternalControl("Off")
- return "Done"
- except Exception as E:
- print("\nGame Time Loop Error: {}".format(str(E)))
- GoveeInternalControl("Off")
- return "Error"
-
-GoveeInternalControl("Off")
-
-appFocusThread = threading.Thread(name="AppThread",target=DetectFocusChange)
-appFocusThread.start()
-
-GameTime()
-# GoveeInternalControl("On")
-# time.sleep(2)
-# GoveeInternalControl("BrightLevel",100) #1-100% expressed as an integer
-# time.sleep(2)
-# GoveeInternalControl("Color",color=(255,0,0))
-# time.sleep(2)
-# GoveeInternalControl("BrightLevel",50) #1-100% expressed as an integer
-# time.sleep(2)
-# GoveeInternalControl("Color",color=(0,255,0))
-# time.sleep(2)
-# GoveeInternalControl("BrightLevel",10) #1-100% expressed as an integer
-# time.sleep(2)
-# GoveeInternalControl("Off")
diff --git a/old/GameSync2023.py b/old/GameSync2023.py
deleted file mode 100644
index 82d287c..0000000
--- a/old/GameSync2023.py
+++ /dev/null
@@ -1,179 +0,0 @@
-from threading import Thread
-import ctypes
-from ctypes import *
-import ctypes.wintypes
-import json
-import socket
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-import sys,os
-DeviceIP = "10.1.1.43" #INSERT YOUR DEVICE IP
-
-def GoveeLocalControl(Command,brightness=0,color=None,Loop=False,UDP_IP='',UDP_PORT=4003,printStatus=True,errorCount=0):
- lastColor = None
- global sock
-
- def SendCommand(message,individualIP,UDP_PORT):
- jsonResult = json.dumps(message)
- print("Sending: {} to {}".format(Command,individualIP))
- sock.sendto(bytes(jsonResult, "utf-8"), (individualIP, UDP_PORT))
-
- try:
- if Command == "Color":
- message = {
- "msg":{
- "cmd":"colorwc",
- "data":{
- "color":{"r":color[0],"g":color[1],"b":color[2]},
- "colorTemInKelvin":0
- }
- }
- }
- elif Command == "On":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":1,
- }
- }
- }
- elif Command == "Off":
- message = {
- "msg":{
- "cmd":"turn",
- "data":{
- "value":0,
- }
- }
- }
- elif Command == "BrightLevel":
- message = {
- "msg":{
- "cmd":"brightness",
- "data":{
- "value":brightness,
- }
- }
- }
- else:
- raise ValueError("Bad Command: {}".format(Command))
-
- #To print or not to print
- if Loop == False and printStatus == True:
- print("Govee Internal Control: {}".format(Command))
-
- #Send command from above
- SendCommand(message,UDP_IP,UDP_PORT)
- except Exception as E:
- errorCount += 1
- if errorCount > 3:
- print("Govee Internal Control Error Recursion Exit: {}".format(str(E)))
- return
- else:
- print("Govee Internal Control Error: {}".format(str(E)))
- return GoveeLocalControl(Command,brightness,color,Loop,UDP_IP,UDP_PORT,printStatus,errorCount)
-
-
-def main(DeviceIP):
- global boxSize
- global sock
- errorNotified = False
- lastColor = None
- handle = None
- whndl = None
- user32 = ctypes.windll.user32
- gdi32 = ctypes.windll.gdi32
-
- screen_width = user32.GetSystemMetrics(0)
- screen_height = user32.GetSystemMetrics(1)
- print("Using Screen Dimensions: {} {}".format(screen_width,screen_height))
- middle_x = screen_width // 2
- middle_y = screen_height // 2
-
- # Get the device context for the entire screen
- screen_dc = user32.GetDC(None)
-
- # Create a compatible device context
- mem_dc = gdi32.CreateCompatibleDC(screen_dc)
-
- # Create a bitmap compatible with the screen device context
- bmp = gdi32.CreateCompatibleBitmap(screen_dc, 1, 1)
-
- # Select the bitmap object into the compatible device context
- gdi32.SelectObject(mem_dc, bmp)
-
- # Define the value of SRCCOPY manually
- SRCCOPY = 0xCC0020
-
-
-
- try:
- print("Game Time Function: Turning Lights On")
- GoveeLocalControl(Command="On",UDP_IP=DeviceIP)
- GoveeLocalControl(Command="BrightLevel",UDP_IP=DeviceIP,brightness=100)
- while(1):
- # Copy the pixel color from the screen to the bitmap
- gdi32.BitBlt(mem_dc, 0, 0, 1, 1, screen_dc, middle_x, middle_y, SRCCOPY)
-
- # Retrieve the color value of the pixel
- pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
-
- # Extract the individual color components (red, green, blue)
- red = pixel_color & 0xFF
- green = (pixel_color >> 8) & 0xFF
- blue = (pixel_color >> 16) & 0xFF
- pixelColor = (red,green,blue)
-
- try:
- #Send colors
- if pixelColor == lastColor:
- # print("Same Color")
- continue
- elif "Error" in pixelColor:
- if errorNotified == False:
- print("Error In Colors Loop: {}".format(pixelColor))
- errorNotified = True
- time.sleep(5)
- continue
- else:
- # print(pixelColor)
- lastColor = pixelColor
- errorNotified = False
-
- message = {
- "msg":{
- "cmd":"colorwc",
- "data":{
- "color":{"r":red,"g":green,"b":blue},
- "colorTemInKelvin":0
- }
- }
- }
- # print("Sending: {}".format(Command))
- sock.sendto(bytes(json.dumps(message), "utf-8"), (DeviceIP, 4003))
- except Exception as E:
- print("Govee Game Time Inner Loop Error: {}".format(str(E)))
- #If for some reason we break from loop and we return from this thread, we want to power down lights.
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- gdi32.DeleteDC(mem_dc)
- gdi32.DeleteDC(screen_dc)
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- raise NameError("NormalReturn")
- except Exception as E:
- if "NormalReturn" in str(E):
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- return "NormalExit"
- else:
- print("Govee Game Time Loop Error: {}".format(str(E)))
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- return "Error"
-
-try:
- print("Press Ctrl + C To Quit (Lights Should Turn Off)\n\n")
- main(DeviceIP)
-except KeyboardInterrupt:
- GoveeLocalControl(Command="Off",UDP_IP=DeviceIP)
- try:
- sys.exit(130)
- except SystemExit:
- os._exit(130)
\ No newline at end of file
diff --git a/old/GameSync2024.py b/old/GameSync2024.py
deleted file mode 100644
index 8ff7a00..0000000
--- a/old/GameSync2024.py
+++ /dev/null
@@ -1,335 +0,0 @@
-import base64
-import ctypes
-import ctypes.wintypes
-import json
-import os
-import socket
-import sys
-import time
-from ctypes import *
-
-import numpy as np
-
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-DeviceIP = ["192.168.0.108", "192.168.0.248"]
-# DeviceIP = ["10.1.1.54","10.1.1.43","10.1.1.44"] #INSERT YOUR DEVICE IP(S)
-
-
-def GoveeLocalControl(
- Command,
- brightness=0,
- color=None,
- Loop=False,
- UDP_IP="",
- UDP_PORT=4003,
- printStatus=False,
- errorCount=0,
-):
- lastColor = None
- global sock
-
- def SendCommand(message, individualIP, UDP_PORT):
- jsonResult = json.dumps(message)
- # jsonResult = jsonResult.replace("'",'"')
- # print("Sending: {} to {} - {}".format(Command, individualIP, jsonResult))
- sock.sendto(bytes(jsonResult, "utf-8"), (individualIP, UDP_PORT))
-
- try:
- if Command == "Color":
- message = {
- "msg": {
- "cmd": "colorwc",
- "data": {
- "color": {"r": color[0], "g": color[1], "b": color[2]},
- "colorTemInKelvin": 0,
- },
- }
- }
- elif Command == "On":
- message = {
- "msg": {
- "cmd": "turn",
- "data": {
- "value": 1,
- },
- }
- }
- elif Command == "Off":
- message = {
- "msg": {
- "cmd": "turn",
- "data": {
- "value": 0,
- },
- }
- }
- elif Command == "SegmentInit":
- message = {"msg": {"cmd": "status", "data": {}}}
- for individualIP in UDP_IP:
- SendCommand(message, individualIP, UDP_PORT)
- message = {
- "msg": {
- "cmd": "razer",
- "data": {
- "pt": "uwABsQEK",
- },
- }
- }
- for individualIP in UDP_IP:
- SendCommand(message, individualIP, UDP_PORT)
- message = {"msg": {"cmd": "status", "data": {}}}
- time.sleep(2)
- elif Command == "SegmentTerm":
- message = {"msg": {"cmd": "status", "data": {}}}
- for individualIP in UDP_IP:
- SendCommand(message, individualIP, UDP_PORT)
- message = {
- "msg": {
- "cmd": "razer",
- "data": {
- "pt": "uwABsQAL",
- },
- }
- }
- for individualIP in UDP_IP:
- SendCommand(message, individualIP, UDP_PORT)
- message = {"msg": {"cmd": "status", "data": {}}}
- time.sleep(2)
- elif Command == "SegmentColor":
- gradientFlag = 1
- color1 = np.array(color[0])
- color2 = np.array(color[1])
- byteArray = [187, 0, 32, 176, gradientFlag, 20]
-
- # Interpolate between color1 and color2
- for t in np.linspace(0, 1, 20):
- interpolated_color = (1 - t) * color1 + t * color2
- byteArray.extend(interpolated_color.astype(int))
-
- num2 = 0
- for byte in byteArray:
- num2 ^= byte
- byteArray.append(num2)
- finalSendValue = base64.b64encode(bytes(byteArray)).decode()
- message = {
- "msg": {
- "cmd": "razer",
- "data": {
- "pt": finalSendValue,
- },
- }
- }
- # elif Command == "SegmentColor":
- # gradientFlag = 1
- # r = color[0][0]
- # g = color[0][1]
- # b = color[0][2]
- # gradientFlag = 0
- # # byteArray = [187,0,32,176,gradientFlag,10,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b,r,g,b]
- # byteArray = [
- # 187,
- # 0,
- # 32,
- # 176,
- # gradientFlag,
- # 10,
- # color[0][0],
- # color[0][1],
- # color[0][2],
- # color[0][0],
- # color[0][1],
- # color[0][2],
- # color[0][0],
- # color[0][1],
- # color[0][2],
- # color[0][0],
- # color[0][1],
- # color[0][2],
- # color[0][0],
- # color[0][1],
- # color[0][2],
- # color[1][0],
- # color[1][1],
- # color[1][2],
- # color[1][0],
- # color[1][1],
- # color[1][2],
- # color[1][0],
- # color[1][1],
- # color[1][2],
- # color[1][0],
- # color[1][1],
- # color[1][2],
- # color[1][0],
- # color[1][1],
- # color[1][2],
- # ]
- # num2 = 0
- # for byte in byteArray:
- # num2 ^= byte
- # byteArray.append(num2)
- # finalSendValue = base64.b64encode(bytes(byteArray)).decode()
- # message = {
- # "msg": {
- # "cmd": "razer",
- # "data": {
- # "pt": finalSendValue,
- # },
- # }
- # }
- elif Command == "BrightLevel":
- message = {
- "msg": {
- "cmd": "brightness",
- "data": {
- "value": brightness,
- },
- }
- }
- else:
- raise ValueError("Bad Command: {}".format(Command))
-
- # To print or not to print
- if Loop == False and printStatus == True:
- print("Govee Internal Control: {}".format(Command))
-
- # Send command from above
- for individualIP in UDP_IP:
- SendCommand(message, individualIP, UDP_PORT)
- except Exception as E:
- errorCount += 1
- if errorCount > 3:
- print("Govee Internal Control Error Recursion Exit: {}".format(str(E)))
- return
- else:
- print("Govee Internal Control Error: {}".format(str(E)))
- return GoveeLocalControl(
- Command,
- brightness,
- color,
- Loop,
- UDP_IP,
- UDP_PORT,
- printStatus,
- errorCount,
- )
-
-
-def main(DeviceIP):
- global boxSize
- global sock
- errorNotified = False
- lastColor = None
- handle = None
- whndl = None
- user32 = ctypes.windll.user32
- gdi32 = ctypes.windll.gdi32
-
- screen_width = user32.GetSystemMetrics(0)
- screen_height = user32.GetSystemMetrics(1)
- print("Using Screen Dimensions: {} {}".format(screen_width, screen_height))
- middle_x = screen_width // 2
- middle_y = screen_height // 2
- chunk = int(screen_width / 4)
-
- # Get the device context for the entire screen
- screen_dc = user32.GetDC(None)
-
- # Create a compatible device context
- mem_dc = gdi32.CreateCompatibleDC(screen_dc)
-
- # Create a bitmap compatible with the screen device context
- bmp = gdi32.CreateCompatibleBitmap(screen_dc, 1, 1)
-
- # Select the bitmap object into the compatible device context
- gdi32.SelectObject(mem_dc, bmp)
-
- # Define the value of SRCCOPY manually
- SRCCOPY = 0xCC0020
-
- try:
- print("Game Time Function: Turning Lights On")
- while 1:
- # Copy the pixel color from the screen to the bitmap
- gdi32.BitBlt(
- mem_dc, 0, 0, 1, 1, screen_dc, middle_x - chunk, middle_y, SRCCOPY
- )
-
- # Retrieve the color value of the pixel
- pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
-
- # Extract the individual color components (red, green, blue)
- red = pixel_color & 0xFF
- green = (pixel_color >> 8) & 0xFF
- blue = (pixel_color >> 16) & 0xFF
- pixelColor1 = (red, green, blue)
-
- gdi32.BitBlt(
- mem_dc, 0, 0, 1, 1, screen_dc, middle_x + chunk, middle_y, SRCCOPY
- )
-
- # Retrieve the color value of the pixel
- pixel_color = gdi32.GetPixel(mem_dc, 0, 0)
-
- # Extract the individual color components (red, green, blue)
- red = pixel_color & 0xFF
- green = (pixel_color >> 8) & 0xFF
- blue = (pixel_color >> 16) & 0xFF
- pixelColor2 = (red, green, blue)
- # print(str(middle_x-chunk)+"-"+str(pixelColor1))
- # print(str(middle_x+chunk)+"-"+str(pixelColor2))
-
- try:
- # Send colors
- if "Error" in pixelColor1 or "Error" in pixelColor2:
- if errorNotified == False:
- print("Error In Colors Loop: {}".format(pixelColor))
- errorNotified = True
- time.sleep(5)
- continue
- else:
- # print(pixelColor)
- lastColor = pixelColor1
- errorNotified = False
- GoveeLocalControl(
- Command="SegmentColor",
- UDP_IP=DeviceIP,
- color=(pixelColor1, pixelColor2),
- )
- except Exception as E:
- print("Govee Game Time Inner Loop Error: {}".format(str(E)))
- # If for some reason we break from loop and we return from this thread, we want to power down lights.
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- gdi32.DeleteDC(mem_dc)
- gdi32.DeleteDC(screen_dc)
- GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
- raise NameError("NormalReturn")
- except Exception as E:
- if "NormalReturn" in str(E):
- print("Game Time Function: Turning Lights Off. Thread Is Dead.")
- return "NormalExit"
- else:
- print("Govee Game Time Loop Error: {}".format(str(E)))
- GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
- return "Error"
-
-
-try:
- print("Press Ctrl + C To Quit (Lights Should Turn Off)\n\n")
- GoveeLocalControl(Command="SegmentTerm", UDP_IP=DeviceIP)
- GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
- time.sleep(1)
- GoveeLocalControl(Command="On", UDP_IP=DeviceIP)
- GoveeLocalControl(Command="BrightLevel", UDP_IP=DeviceIP, brightness=30)
- time.sleep(1)
- GoveeLocalControl(Command="SegmentInit", UDP_IP=DeviceIP)
- time.sleep(1)
- # GoveeLocalControl(Command="SegmentColor", color=[(255, 0, 0)], UDP_IP=DeviceIP)
- main(DeviceIP)
-except KeyboardInterrupt:
- GoveeLocalControl(Command="SegmentTerm", UDP_IP=DeviceIP)
- GoveeLocalControl(Command="Off", UDP_IP=DeviceIP)
- try:
- sys.exit(130)
- except SystemExit:
- os._exit(130)
diff --git a/old/GameSync2024v2.py b/old/GameSync2024v2.py
deleted file mode 100644
index 6e0ebf2..0000000
--- a/old/GameSync2024v2.py
+++ /dev/null
@@ -1,138 +0,0 @@
-import base64
-import time
-from enum import Enum
-from typing import List, Tuple
-
-from PIL import ImageGrab
-from pydantic import BaseModel, Field
-
-# Configuration Constants
-DEVICE_IP = ["10.1.1.54", "10.1.1.43"]
-DEVICE_IP: list[str] = ["192.168.0.108", "192.168.0.248"]
-UDP_PORT = 4003
-NUM_COLORS = 20
-
-
-# Enums for Power State
-class PowerState(Enum):
- OFF = 0
- ON = 1
-
-
-# Pydantic Models
-class Color(BaseModel):
- r: int = Field(..., ge=0, le=255)
- g: int = Field(..., ge=0, le=255)
- b: int = Field(..., ge=0, le=255)
-
-
-class Command(BaseModel):
- cmd: str
- data: BaseModel
-
-
-class PowerData(BaseModel):
- value: PowerState
-
-
-class BrightnessData(BaseModel):
- value: int
-
-
-class ColorData(BaseModel):
- color: Color
- colorTemInKelvin: int = 0
-
-
-class SegmentData(BaseModel):
- pt: str
-
-
-class PowerCommand(Command):
- cmd: str = "turn"
- data: PowerData
-
-
-class BrightnessCommand(Command):
- cmd: str = "brightness"
- data: BrightnessData
-
-
-class ColorCommand(Command):
- cmd: str = "colorwc"
- data: ColorData
-
-
-class SegmentCommand(Command):
- cmd: str = "razer"
- data: SegmentData
-
-
-# Utility Functions
-def capture_screen_and_process_colors(
- num_devices: int,
-) -> List[List[Tuple[int, int, int]]]:
- screen_image = ImageGrab.grab()
- screen_width, screen_height = screen_image.size
- vertical_positions = [
- int(screen_height * i / NUM_COLORS) for i in range(NUM_COLORS)
- ]
- horizontal_sections = [
- int(screen_width * (i + 1) / (num_devices + 1)) for i in range(num_devices)
- ]
- return [
- [screen_image.getpixel((x, y)) for y in vertical_positions]
- for x in horizontal_sections
- ]
-
-
-def format_and_send_colors(
- controller: LightController, colors: List[List[Tuple[int, int, int]]]
-):
- for ip, colors_list in zip(controller.ips, colors):
- byteArray = [187, 0, 32, 176, 1, NUM_COLORS] + [
- c for color in colors_list for c in color
- ]
- checksum = sum(byteArray) % 256
- byteArray.append(checksum)
- encoded_data = base64.b64encode(bytes(byteArray)).decode()
- segment_data = SegmentData(pt=encoded_data)
- command = SegmentCommand(data=segment_data)
- controller.send_command(command)
-
-
-def game_loop(controller: LightController):
- try:
- for ip in controller.ips:
- controller.send_command(PowerCommand(data=PowerData(value=PowerState.ON)))
- controller.send_command(BrightnessCommand(data=BrightnessData(value=100)))
- controller.send_command(SegmentCommand(data=SegmentData(pt="uwABsQEK")))
- time.sleep(2)
-
- while True:
- color_data = capture_screen_and_process_colors(len(controller.ips))
- format_and_send_colors(controller, color_data)
- time.sleep(1)
- except KeyboardInterrupt:
- print("Operation stopped by user.")
- finally:
- for ip in controller.ips:
- controller.send_command(
- ColorCommand(
- data=ColorData(color=Color(r=255, g=165, b=0), colorTemInKelvin=0)
- )
- )
- time.sleep(2)
- for ip in controller.ips:
- controller.send_command(SegmentCommand(data=SegmentData(pt="uwABsQAL")))
- controller.send_command(PowerCommand(data=PowerData(value=PowerState.OFF)))
-
-
-def main():
- controller = LightController(DEVICE_IP)
- print("Press Ctrl+C to quit.")
- game_loop(controller)
-
-
-if __name__ == "__main__":
- main()
diff --git a/old/GetGoveeDevices.py b/old/GetGoveeDevices.py
deleted file mode 100644
index 43384d3..0000000
--- a/old/GetGoveeDevices.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import json
-import socket
-import struct
-
-
-def discover_devices(send_group, send_port, receive_port, message, timeout=5):
- # Set up the sending socket with TTL for multicast
- send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
- ttl = struct.pack("b", 1) # Time-to-live of the multicast message
- send_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
-
- # Set up the receiving socket
- recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
- recv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- recv_sock.bind(("", receive_port))
-
- # Join multicast group
- mreq = struct.pack("4sl", socket.inet_aton(send_group), socket.INADDR_ANY)
- recv_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
-
- # Send the multicast message
- json_message = json.dumps(message)
- print(f"Sending: {json_message}")
- send_sock.sendto(bytes(json_message, "utf-8"), (send_group, send_port))
- send_sock.close()
-
- # Listen for responses
- recv_sock.settimeout(timeout)
- devices = []
- try:
- while True:
- data, addr = recv_sock.recvfrom(10240)
- print(f"Received response from {addr}: {data}")
- devices.append(addr[0])
- except socket.timeout:
- print("Listening timeout reached. No more responses.")
- finally:
- recv_sock.close()
-
- return devices
-
-
-if __name__ == "__main__":
- send_group = "239.255.255.250"
- send_port = 4001
- receive_port = 4002
- message = {
- "msg": {
- "cmd": "scan",
- "data": {
- "account_topic": "reserve",
- },
- }
- }
- devices = discover_devices(send_group, send_port, receive_port, message)
- print("Discovered devices:", devices)
diff --git a/old/README.md b/old/README.md
deleted file mode 100644
index e142368..0000000
--- a/old/README.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# GoveeSync
-This code will allow you to use Govee internal UDP api to control your device as well as sync what is on the screen. You need to install the below libs before running:
-pip install wmi
-pip install pywin32
-
-# Constants
-Update the device IP of the device you want to control on line 21.
-
-# No Frills Screen Sync Script
-
-If you don't want to get into the different functions and you just want something simple you can run and have work, just run GameSync2023.py file.
-
-- Replace line 10 with your device IP, for example: DeviceIP = "192.168.0.1"
-- Open cmd prompt then simply run with - python c:\PathToFile\GameSync2023.py
-- You can simply do Ctrl + C to exit when you want to end the light sync.
-
-
-# Full Featured Code With Many Supported Commands - All of the below commands are implemented within GameSync.py
-
-To turn the device on/off:
-GoveeInternalControl("On")
-GoveeInternalControl("Off")
-
-To set the brightness:
-GoveeInternalControl("BrightLevel",100) #1-100% expressed as an integer
-GoveeInternalControl("BrightLevel",50) #1-100% expressed as an integer
-GoveeInternalControl("BrightLevel",10) #1-100% expressed as an integer
-
-To change the color:
-GoveeInternalControl("Color",color=(0,255,0)) #Color expressed as a RGB tuple
-GoveeInternalControl("Color",color=(255,0,0)) #Color expressed as a RGB tuple
-
-To go into game mode where the screen will sync to the device:
-Do note: in this mode the code will attempt to lock onto the window that is in focus.
There is a box size constant you can mess around with if you want to and see if you get better results.
This mode essentially works by creating a square in the center of the app's window. We sample a pixel at each of the four corners of the square, get the color values, then average those 4 color values together. That averaged out value is then sent to the Govee lights over UDP api.
-
-GameTime()
-
-# Testing / Searching for devices - This is optional if you want to search for available Govee LAN api devices on your network.
-Download UDPReceiver.py and UDPSender.py
-
-- Start CMD prompt, navigate to the folder where you downloaded the above files. Then type: python UDPReceiver.py
This should start a UDP multicast listener on port 4002
-- Once the listener is started open another CMD prompt. Navigate to the folder, and type: python UDPSender.py
This should output any Govee devices found to the shell.
-
-
-
diff --git a/old/ScreenSync.py b/old/ScreenSync.py
deleted file mode 100644
index 382cff6..0000000
--- a/old/ScreenSync.py
+++ /dev/null
@@ -1,401 +0,0 @@
-import base64
-import signal
-import socket
-import time
-from enum import Enum
-from typing import Union
-
-import numpy as np
-from PIL import Image, ImageEnhance, ImageGrab
-from pydantic import BaseModel, Field
-
-DEBUG = False
-UDP_PORT = 4003
-MAX_LED_COLOR_GRADIENT = 10
-
-
-class PowerState(Enum):
- OFF = 0
- ON = 1
-
-
-class Color(BaseModel):
- r: int = Field(default=205, ge=0, le=255)
- g: int = Field(default=125, ge=0, le=255)
- b: int = Field(default=0, ge=0, le=255)
-
- @property
- def rgb(self):
- return (self.r, self.g, self.b)
-
-
-class PowerData(BaseModel):
- value: PowerState
-
-
-class BrightnessData(BaseModel):
- value: int
-
-
-class ColorData(BaseModel):
- color: Color
- colorTemInKelvin: int = 0
-
-
-class SegmentData(BaseModel):
- pt: str
-
-
-class Command(BaseModel):
- cmd: str
- data: Union[PowerData, BrightnessData, ColorData, SegmentData]
-
-
-class Message(BaseModel):
- msg: Command
-
-
-class DeviceScanCommand(Command):
- cmd: str = "scan"
- data: dict[str, str] = {"account_topic": "reserve"}
-
-
-class PowerCommand(Command):
- cmd: str = "turn"
- data: PowerData
-
-
-class BrightnessCommand(Command):
- cmd: str = "brightness"
- data: BrightnessData
-
-
-class ColorCommand(Command):
- cmd: str = "colorwc"
- data: ColorData
-
-
-class SegmentCommand(Command):
- cmd: str = "razer"
- data: SegmentData
-
-
-class GoveeLightDevice:
- def __init__(self, ip: str, name: str, screen_positions: list[tuple[int, int]]):
- self.ip = ip
- self.name = name
- self.screen_positions = screen_positions
-
- def _send_command(self, command: Command):
- message = Message(msg=command).model_dump_json()
-
- with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
- sock.sendto(message.encode(), (self.ip, UDP_PORT))
- if DEBUG:
- print(f"Command sent to {self.ip}: {message}")
-
- def power_on(self):
- power_on_command = PowerCommand(data=PowerData(value=PowerState.ON))
- self._send_command(power_on_command)
-
- def power_off(self):
- power_off_command = PowerCommand(data=PowerData(value=PowerState.OFF))
- self._send_command(power_off_command)
-
- def set_color(self, color: Color):
- color_command = ColorCommand(data=ColorData(color=color))
- self._send_command(color_command)
-
- def set_brightness(self, brightness: int):
- brightness_command = BrightnessCommand(data=BrightnessData(value=brightness))
- self._send_command(brightness_command)
-
- def initialize_segment(self):
- """Initializes a segment to be set with set_segment_colors."""
- segment_command = SegmentCommand(data=SegmentData(pt="uwABsQEK"))
- self._send_command(segment_command)
-
- def terminate_segment(self):
- """Returns the color to the state before the segment was initialized."""
- segment_command = SegmentCommand(data=SegmentData(pt="uwABsQAL"))
- self._send_command(segment_command)
-
- def set_segment_colors(self, list_of_colors: list[Color], gradient=True):
- """Sets the colors of the segment to the given list of colors.
- color list of size (min 2, max 10)
-
- probably move "intrepolation" and "resolution" to some other place.
-
- colors ordering goes from bottom to top for a lamp.
- not sure about a strip.
- but probably power source to the end of the strip.
- """
-
- assert (
- 2 <= len(list_of_colors) <= MAX_LED_COLOR_GRADIENT
- ), f"Color list must be between 1 and 10, got {len(list_of_colors)}"
-
- segment_color_data = self._get_segment_color_data(list_of_colors, gradient)
- segment_command = SegmentCommand(data=segment_color_data)
- self._send_command(segment_command)
-
- def _get_segment_color_data(
- self, list_of_colors: list[Color], gradient=True
- ) -> SegmentData:
- """Returns the SegmentData object for the given list of colors.
-
- The segment data is a base64 encoded string that represents the colors to be set.
- I have no idea what the values for the first 4 bytes are.
- I assume the 6th byte is the number of colors.
- """
- gradient_flag = 1 if gradient else 0
- byte_array = [187, 0, 32, 176, gradient_flag, len(list_of_colors)]
-
- for color in list_of_colors:
- byte_array.extend(color.rgb)
-
- num2 = 0
- for byte in byte_array:
- num2 ^= byte
- byte_array.append(num2)
- final_send_value = base64.b64encode(bytes(byte_array)).decode()
- return SegmentData(pt=final_send_value)
-
-
-def get_device_location_indices(
- column_indices: list[int] | None = None, row_indices: list[int] | None = None
-):
- """
- screen example for MAX_LED_COLOR_GRADIENT = 10
-
- column numbers
- 0 1 2 3 4 5 6 7 8 9
- 0 x x x x x x x x x x
- 1 x x x x x x x x x x
- 2 x x x x x x x x x x
- 3 x x x x x x x x x x
- 4 x x x x x x x x x x
- 5 x x x x x x x x x x
- 6 x x x x x x x x x x
- 7 x x x x x x x x x x
- 8 x x x x x x x x x x
- 9 x x x x x x x x x x
-
- My device has index 0 at the bottom, so I need to reverse the row indices.
- """
- if column_indices is None:
- column_indices = list(range(MAX_LED_COLOR_GRADIENT))
- if row_indices is None:
- # light lamp colors are ordered from bottom to top
- row_indices = list(range(MAX_LED_COLOR_GRADIENT)[::-1])
- screen_indices = [row_indices, column_indices]
- return screen_indices
-
-
-def example_usage():
- default_color = Color(r=255, g=165, b=0)
- left_column_screen_indices = [(0, i) for i in range(MAX_LED_COLOR_GRADIENT)]
- left_column_device = GoveeLightDevice(
- "192.168.0.248", "Govee Light Left", left_column_screen_indices
- )
-
- try:
- # Example usage of the PowerCommand model
- left_column_device.power_on()
-
- # Example usage of the ColorCommand model
- left_column_device.set_color(default_color)
- time.sleep(1)
-
- # Example usage of the BrightnessCommand model
- left_column_device.set_brightness(50)
- time.sleep(1)
- left_column_device.set_brightness(100)
- time.sleep(1)
-
- # Example usage of the SegmentCommand model
- left_column_device.initialize_segment()
-
- left_column_device.set_segment_colors(
- [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0)],
- )
- time.sleep(3)
- left_column_device.set_segment_colors(
- [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0), Color(r=0, g=0, b=25)],
- )
- time.sleep(3)
-
- left_column_device.terminate_segment()
-
- except Exception as e:
- print(e)
- finally:
- left_column_device.power_off()
-
-
-def capture_screen_and_process_colors() -> np.ndarray:
- # Capture the entire screen
- screen_image = ImageGrab.grab()
- resize_factor = 10
-
- # Resize image to a manageable size that maintains the aspect ratio of 10x10 blocks
- resized_image = screen_image.resize(
- (MAX_LED_COLOR_GRADIENT * resize_factor, MAX_LED_COLOR_GRADIENT * resize_factor)
- )
-
- # Increase the saturation by 1.5 times
- converter = ImageEnhance.Color(resized_image)
- saturated_image = converter.enhance(2.5)
-
- # Convert image to numpy array for processing
- color_matrix = np.array(saturated_image)
-
- # Reshape the array to (10, 10, 10, 10, 3) where each 10x10 block's pixels are separately accessible
- color_matrix = color_matrix.reshape(
- (
- MAX_LED_COLOR_GRADIENT,
- resize_factor,
- MAX_LED_COLOR_GRADIENT,
- resize_factor,
- 3,
- )
- )
-
- # I favor red > green > blue for the most "colorful pixel". Let's scale the colors accordingly
- biased_color_matrix = color_matrix.copy()
- biased_color_matrix[..., 0] *= 2
- biased_color_matrix[..., 1] *= 1
- biased_color_matrix[..., 2] *= 1
-
- # Compute variance across each pixel to find the most "colorful" pixel
- pixel_variances = np.var(biased_color_matrix, axis=4)
-
- # Transpose the array so that the 10x10 blocks are along the last two axes
- pixel_variances_transposed = pixel_variances.transpose(0, 2, 1, 3)
-
- # Flatten the last two dimensions
- pixel_variances_flattened = pixel_variances_transposed.reshape(
- MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT, -1
- )
-
- # Compute argmax over the flattened dimension
- max_colorful_indices = np.unravel_index(
- pixel_variances_flattened.argmax(axis=2), (resize_factor, resize_factor)
- )
-
- # For each block, get the most colorful pixel
- most_colorful_pixels = color_matrix[
- np.arange(MAX_LED_COLOR_GRADIENT)[:, None],
- max_colorful_indices[0],
- np.arange(MAX_LED_COLOR_GRADIENT),
- max_colorful_indices[1],
- ]
-
- most_colorful_pixels = most_colorful_pixels.reshape(
- MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT, 3
- )
-
- if DEBUG:
- saturated_image.save("debug_saturated_image.png")
-
- most_colorful_pixels_image = Image.fromarray(
- most_colorful_pixels.astype(np.uint8), "RGB"
- )
- most_colorful_pixels_image.save("debug_preview.png")
- # raise an exception to stop the program and show the image
- raise Exception("Debug mode enabled. Stopping program to show image.")
-
- return most_colorful_pixels
-
-
-class FrameCounter:
- def __init__(self):
- self.last_100_fps = []
- self.start_time = time.time()
- self.previous_time = time.time()
-
- def update_and_print(self):
- elapsed_time = time.time() - self.previous_time
- self.previous_time = time.time()
- time_since_start = time.time() - self.start_time
- fps = 1 / elapsed_time
- self.last_100_fps = self.last_100_fps[-99:]
- self.last_100_fps.append(fps)
- average_fps = (
- sum(self.last_100_fps) / len(self.last_100_fps) if self.last_100_fps else 0
- )
- print(
- f"\rFPS: {fps:.1f} - Average FPS {average_fps:.1f} - Total Time:{time_since_start:.1f}",
- end="",
- )
-
- return fps
-
- def reset(self):
- self.frame_count = 0
- self.start_time = time.time()
-
-
-def game_loop(devices: list[GoveeLightDevice]):
- frame_counter = FrameCounter()
- try:
- for device in devices:
- device.power_on()
- time.sleep(0.1)
- device.set_brightness(100)
- time.sleep(0.1)
- device.set_color(Color())
-
- time.sleep(0.5)
-
- for device in devices:
- device.initialize_segment()
-
- while True:
- screen_colors = capture_screen_and_process_colors()
- for device in devices:
- selected_rows = screen_colors[device.screen_positions[0], :, :]
- selected_columns = selected_rows[:, device.screen_positions[1], :]
- color_data = [
- Color(r=r, g=g, b=b)
- for r, g, b in selected_columns.mean(axis=1).astype(int)
- ]
- device.set_segment_colors(color_data)
- frame_counter.update_and_print()
- # write to terminal a fps counter (but don't print it every frame, instead update it in place)
-
- except KeyboardInterrupt:
- print("Operation stopped by user.")
- finally:
- for device in devices:
- device.terminate_segment()
- time.sleep(0.5)
- device.power_off()
-
-
-def run():
- left_column_device = GoveeLightDevice(
- "192.168.0.248", "Govee Light Right", get_device_location_indices([7, 8, 9])
- )
- right_column_device = GoveeLightDevice(
- "192.168.0.108",
- "Govee Light Left",
- get_device_location_indices([0, 1, 2]),
- )
- devices = [left_column_device, right_column_device]
-
- def power_off_devices_and_exit():
- if left_column_device is not None:
- left_column_device.power_off()
- if right_column_device is not None:
- right_column_device.power_off()
- exit()
-
- print("Starting Govee Light Controller...")
- signal.signal(signal.SIGTERM, lambda signum, frame: power_off_devices_and_exit())
- signal.signal(signal.SIGINT, lambda signum, frame: power_off_devices_and_exit())
- game_loop(devices=devices)
-
-
-if __name__ == "__main__":
- run()
diff --git a/old/UDPReceiver.py b/old/UDPReceiver.py
deleted file mode 100644
index 564c92d..0000000
--- a/old/UDPReceiver.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import socket
-import struct
-
-MCAST_GRP = "239.255.255.250"
-MCAST_PORT = 4002
-
-sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
-sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-
-sock.bind(('', MCAST_PORT))
-mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)
-
-sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
-
-while True:
- print("Listening on {}".format(MCAST_PORT))
- if sock.recv:
- print(sock.recv(10240))
- break
\ No newline at end of file
diff --git a/old/UDPSender.py b/old/UDPSender.py
deleted file mode 100644
index 58179d5..0000000
--- a/old/UDPSender.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import json,time,socket,struct
-message = {
- "msg":{
- "cmd":"scan",
- "data":{
- "account_topic":"reserve",
- }
- }
-}
-import socket
-group = "239.255.255.250"
-port = 4001
-# 2-hop restriction in network
-ttl = 2
-sock = socket.socket(socket.AF_INET,
- socket.SOCK_DGRAM,
- socket.IPPROTO_UDP)
-sock.setsockopt(socket.IPPROTO_IP,
- socket.IP_MULTICAST_TTL,
- ttl)
-jsonResult = json.dumps(message)
-print("Sending: "+jsonResult)
-sock.sendto(bytes(jsonResult, "utf-8"), (group, port))
-
diff --git a/old/controller.py b/old/controller.py
deleted file mode 100644
index bb06ab3..0000000
--- a/old/controller.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import socket
-from typing import List
-
-from models import Command, Message
-
-UDP_PORT = 4003
-
-
-# Controller Class
-class LightController:
- def __init__(self, ips: List[str]):
- self.ips = ips
-
- def send_command(self, command: Command):
- message = Message(msg=command).model_dump_json()
-
- for ip in self.ips:
- with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
- sock.sendto(message.encode(), (ip, UDP_PORT))
- print(f"Command sent to {ip}: {message}")
-
-
-"""
-this is the model.
-Message(msg=PowerCommand(cmd='turn', data=PowerData(value=)))
-
-after model_dump_json() I get this...
-'{"msg":{"cmd":"turn","data":{}}}'
-
-"""
diff --git a/old/debug_console.png b/old/debug_console.png
deleted file mode 100644
index c93dbacdee1488ce5e4af440fde2eac8a96039ff..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 106
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 SegmentData:
- gradient_flag = 1 if gradient else 0
- byte_array = [187, 0, 32, 176, gradient_flag, len(list_of_colors)]
-
- for color in list_of_colors:
- byte_array.extend(color.rgb)
-
- num2 = 0
- for byte in byte_array:
- num2 ^= byte
- byte_array.append(num2)
- final_send_value = base64.b64encode(bytes(byte_array)).decode()
- return SegmentData(pt=final_send_value)
-
-
-def get_column_indices(column_number: int):
- """
- screen example for MAX_LED_COLOR_GRADIENT = 10
-
- column numbers
- 0 1 2 3 4 5 6 7 8 9
- 0 x x x x x x x x x x
- 1 x x x x x x x x x x
- 2 x x x x x x x x x x
- 3 x x x x x x x x x x
- 4 x x x x x x x x x x
- 5 x x x x x x x x x x
- 6 x x x x x x x x x x
- 7 x x x x x x x x x x
- 8 x x x x x x x x x x
- 9 x x x x x x x x x x
-
- My device has index 0 at the bottom, so I need to reverse the row indices.
- """
- column_indices = [column_number] * MAX_LED_COLOR_GRADIENT
- row_indices = list(range(MAX_LED_COLOR_GRADIENT))[::-1]
- screen_indices = [column_indices, row_indices]
- return screen_indices
-
-
-def example_usage():
- default_color = Color(r=255, g=165, b=0)
- # controller = LightController(["192.168.0.108", "192.168.0.248"])
- left_column_screen_indices = [(0, i) for i in range(MAX_LED_COLOR_GRADIENT)]
- left_column_device = GoveeLightDevice(
- "192.168.0.248", "Govee Light Left", left_column_screen_indices
- )
-
- try:
- # Example usage of the PowerCommand model
- left_column_device.power_on()
-
- # Example usage of the ColorCommand model
- left_column_device.set_color(default_color)
- time.sleep(1)
-
- # Example usage of the BrightnessCommand model
- left_column_device.set_brightness(50)
- time.sleep(1)
- left_column_device.set_brightness(100)
- time.sleep(1)
-
- # Example usage of the SegmentCommand model
- left_column_device.initialize_segment()
-
- left_column_device.set_segment_colors(
- [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0)],
- )
- time.sleep(3)
- left_column_device.set_segment_colors(
- [Color(r=25, g=0, b=0), Color(r=0, g=25, b=0), Color(r=0, g=0, b=25)],
- )
- time.sleep(3)
-
- left_column_device.terminate_segment()
-
- except Exception as e:
- print(e)
- finally:
- left_column_device.power_off()
-
-
-def capture_screen_and_process_colors() -> np.ndarray:
- screen_image = ImageGrab.grab()
- resized_image = screen_image.resize(
- (MAX_LED_COLOR_GRADIENT, MAX_LED_COLOR_GRADIENT)
- )
-
- # Increase the saturation
- converter = ImageEnhance.Color(resized_image)
- saturated_image = converter.enhance(1.5)
-
- color_matrix = np.array(saturated_image).transpose(1, 0, 2)
-
- # Replace colors that are too dark
- color_sums = color_matrix.sum(axis=2)
- dark_colors = color_sums < 10
- color_matrix[dark_colors] = [10, 10, 10]
-
- return color_matrix
-
-
-def game_loop(devices: list[GoveeLightDevice]):
- try:
- for device in devices:
- device.power_on()
- time.sleep(0.1)
- device.set_brightness(100)
- time.sleep(0.1)
- device.set_color(Color(r=55, g=165, b=0))
-
- time.sleep(0.5)
-
- for device in devices:
- device.initialize_segment()
-
- while True:
- screen_colors = capture_screen_and_process_colors()
- for device in devices:
- color_data = [
- Color(r=r, g=g, b=b)
- for r, g, b in screen_colors[*device.screen_positions]
- ]
- device.set_segment_colors(color_data)
- time.sleep(0.005) # 200 FPS
- except KeyboardInterrupt:
- print("Operation stopped by user.")
- finally:
- for device in devices:
- device.terminate_segment()
- time.sleep(0.5)
- device.power_off()
-
-
-def run():
- left_column_device = GoveeLightDevice(
- "192.168.0.248", "Govee Light Left", get_column_indices(0)
- )
- right_column_device = GoveeLightDevice(
- "192.168.0.108",
- "Govee Light Right",
- get_column_indices(MAX_LED_COLOR_GRADIENT - 1),
- )
- devices = [left_column_device, right_column_device]
-
- def power_off_devices_and_exit():
- if left_column_device is not None:
- left_column_device.power_off()
- if right_column_device is not None:
- right_column_device.power_off()
- exit()
-
- print("Starting Govee Light Controller...")
- signal.signal(signal.SIGTERM, lambda signum, frame: power_off_devices_and_exit())
- signal.signal(signal.SIGINT, lambda signum, frame: power_off_devices_and_exit())
- game_loop(devices=devices)
-
-
-if __name__ == "__main__":
- run()
diff --git a/old/stuff.md b/old/stuff.md
deleted file mode 100644
index 6dd9b57..0000000
--- a/old/stuff.md
+++ /dev/null
@@ -1,12 +0,0 @@
-Listening on 4002
-b'{"msg":{"cmd":"scan","data":{"ip":"192.168.0.248","device":"53:47:C7:32:34:35:56:8D","sku":"H6076","bleVersionHard":"3.01.01","bleVersionSoft":"1.04.06","wifiVersionHard":"1.00.10","wifiVersionSoft":"1.02.11"}}}'
-
-
-
-(openapi) C:\Users\kaaik\Documents\GitHub\GoveeSync>python UDPReceiver.py
-Listening on 4002
-b'{"msg":{"cmd":"scan","data":{"ip":"192.168.0.108","device":"03:F0:C5:32:34:35:1A:57","sku":"H6076","bleVersionHard":"3.01.01","bleVersionSoft":"1.04.06","wifiVersionHard":"1.00.10","wifiVersionSoft":"1.02.11"}}}'
-
-(openapi) C:\Users\kaaik\Documents\GitHub\GoveeSync>python UDPReceiver.py
-Listening on 4002
-b'{"msg":{"cmd":"scan","data":{"ip":"192.168.0.248","device":"53:47:C7:32:34:35:56:8D","sku":"H6076","bleVersionHard":"3.01.01","bleVersionSoft":"1.04.06","wifiVersionHard":"1.00.10","wifiVersionSoft":"1.02.11"}}}'
\ No newline at end of file
From 239ee7610fc3c78d79f337f5ff76e0b6123f21da Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sun, 14 Apr 2024 13:37:48 -0400
Subject: [PATCH 07/11] Create .gitignore
---
.gitignore | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 169 insertions(+)
create mode 100644 .gitignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f798ea2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,169 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# vscode
+.vscode
+
+# ruff
+.ruff_cache
+
+# macos
+*.DS_Store
From 46c72f93e35a868b21da5be35a9bb4d2fe89ef22 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sun, 14 Apr 2024 14:14:06 -0400
Subject: [PATCH 08/11] add readme and clean up
---
README.md | 100 ++++++++++++++++++
devices.json | 20 ++++
devices_template.json | 20 ++++
.../utils.py => discover_devices.py | 21 ++--
main.py | 46 ++++++++
.../__pycache__/game_sync.cpython-312.pyc | Bin 3877 -> 3869 bytes
.../__pycache__/light_device.cpython-312.pyc | Bin 7162 -> 7158 bytes
.../screen_capture.cpython-312.pyc | Bin 2678 -> 2682 bytes
src/govee_screen_sync/game_sync.py | 7 +-
src/govee_screen_sync/light_device.py | 4 +-
src/govee_screen_sync/main.py | 49 ---------
src/govee_screen_sync/screen_capture.py | 5 +-
12 files changed, 200 insertions(+), 72 deletions(-)
create mode 100644 devices.json
create mode 100644 devices_template.json
rename src/govee_screen_sync/utils.py => discover_devices.py (86%)
create mode 100644 main.py
delete mode 100644 src/govee_screen_sync/main.py
diff --git a/README.md b/README.md
index e69de29..5eaa666 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,100 @@
+Here's the updated README with the new device discovery section:
+
+# Govee Screen Sync
+
+Govee Screen Sync is a Python project that synchronizes Govee LED lights with your computer screen, creating an immersive lighting experience. The lights will change colors based on the content displayed on your screen.
+
+## Prerequisites
+
+Before getting started, ensure that you have the following:
+
+- Python 3.7 or higher installed on your system
+- Poetry package manager installed (see installation instructions [here](https://python-poetry.org/docs/#installation))
+- Govee LED lights compatible with the Govee API
+
+## Installation
+
+1. Clone the repository:
+
+```bash
+git clone https://github.com/yourusername/govee-screen-sync.git
+cd govee-screen-sync
+```
+
+2. Install the required dependencies using Poetry:
+
+```bash
+poetry install
+```
+
+## Device Discovery
+
+To discover your Govee devices and identify their IP addresses, you can run the device discovery script:
+
+1. Activate the Poetry virtual environment:
+
+```bash
+poetry shell
+```
+
+2. Run the device discovery script:
+
+```bash
+python discover_devices.py
+```
+
+The script will attempt to discover your Govee devices automatically. If successful, it will light up each device with a different color of the rainbow and display the corresponding IP address in the console output.
+
+Press Enter to power off the devices.
+
+Use the discovered IP addresses to update the `devices.json` file accordingly.
+
+## Configuration
+
+1. Locate the `devices_template.json` file in the project directory.
+
+2. Make a copy of the `devices_template.json` file and rename it to `devices.json`.
+
+3. Open the `devices.json` file in a text editor.
+
+4. Replace the placeholder IP addresses (`192.168.0.xxx` and `192.168.0.yyy`) with the actual IP addresses of your Govee devices.
+
+5. Customize the device names and screen positions according to your setup. The `screen_positions` array should contain the indices of the screen sections you want the device to be mapped to.
+
+6. Save the `devices.json` file.
+
+7. Customize screen positions (optional):
+
+ The `screen_positions` array in the `devices.json` file allows you to specify which parts of the screen should be mapped to each device. You can customize the values to select specific areas of the screen for each device.
+
+## Usage
+
+1. Activate the Poetry virtual environment:
+
+```bash
+poetry shell
+```
+
+2. Run the script:
+
+```bash
+python main.py
+```
+
+The script will start capturing your screen, processing the colors, and sending commands to your Govee devices to synchronize the lighting with your screen content.
+
+## Troubleshooting
+
+- If the script fails to discover your devices automatically, make sure your devices are powered on and connected to the same network as your computer.
+
+- If the colors don't match your expectations, you can adjust the color processing parameters in the `capture_screen_and_process_colors()` function in the `screen_capture.py` file.
+
+- If the script displays an error message about the `devices.json` file not being found, make sure you have created the file by following the configuration steps above.
+
+## Contributing
+
+Feel free to submit issues and pull requests to improve the project. Contributions are always welcome!
+
+## License
+
+This project is licensed under the [MIT License](LICENSE).
\ No newline at end of file
diff --git a/devices.json b/devices.json
new file mode 100644
index 0000000..b6a0441
--- /dev/null
+++ b/devices.json
@@ -0,0 +1,20 @@
+[
+ {
+ "ip": "192.168.0.248",
+ "name": "Govee Light Right",
+ "screen_positions": [
+ 7,
+ 8,
+ 9
+ ]
+ },
+ {
+ "ip": "192.168.0.108",
+ "name": "Govee Light Left",
+ "screen_positions": [
+ 0,
+ 1,
+ 2
+ ]
+ }
+]
\ No newline at end of file
diff --git a/devices_template.json b/devices_template.json
new file mode 100644
index 0000000..48ee2b4
--- /dev/null
+++ b/devices_template.json
@@ -0,0 +1,20 @@
+[
+ {
+ "ip": "192.168.0.xxx",
+ "name": "Govee Light 1",
+ "screen_positions": [
+ 0,
+ 1,
+ 2
+ ]
+ },
+ {
+ "ip": "192.168.0.yyy",
+ "name": "Govee Light 2",
+ "screen_positions": [
+ 7,
+ 8,
+ 9
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/govee_screen_sync/utils.py b/discover_devices.py
similarity index 86%
rename from src/govee_screen_sync/utils.py
rename to discover_devices.py
index aaff615..10a0823 100644
--- a/src/govee_screen_sync/utils.py
+++ b/discover_devices.py
@@ -2,8 +2,8 @@
import struct
import time
-from govee_screen_sync.light_device import GoveeLightDevice
-from govee_screen_sync.models import Color, DeviceScanMessage
+from src.govee_screen_sync.light_device import GoveeLightDevice
+from src.govee_screen_sync.models import Color, DeviceScanMessage
def discover_devices(
@@ -11,7 +11,7 @@ def discover_devices(
send_port: int = 4001,
receive_port: int = 4002,
message: DeviceScanMessage = DeviceScanMessage(),
- timeout: int = 2,
+ timeout: int = 1,
expected_devices: list[GoveeLightDevice] = None,
):
# Set up the sending socket with TTL for multicast
@@ -41,15 +41,11 @@ def discover_devices(
try:
while True:
data, addr = recv_sock.recvfrom(10240)
- print(
- f"Received response from {addr}: {data} in {time.time() - ti} seconds."
- )
+ print(f"Received response from {addr}: {data} in {time.time() - ti} seconds.")
device_ips.append(addr[0])
# Check if all expected devices have responded
- if expected_devices and all(
- device.ip in device_ips for device in expected_devices
- ):
+ if expected_devices and all(device.ip in device_ips for device in expected_devices):
print("All expected devices have responded.")
break
except socket.timeout:
@@ -62,8 +58,7 @@ def discover_devices(
def color_device_by_ip():
# For each discovered device, create a GoveeLightDevice object
devices = [
- GoveeLightDevice(ip, f"Govee Light {i}", [])
- for i, ip in enumerate(discover_devices())
+ GoveeLightDevice(ip, f"Govee Light {i}", []) for i, ip in enumerate(discover_devices())
]
rainbow_rgb = {
@@ -107,3 +102,7 @@ def color_device_by_ip():
device.power_off()
return devices
+
+
+if __name__ == "__main__":
+ color_device_by_ip()
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..18a9ce4
--- /dev/null
+++ b/main.py
@@ -0,0 +1,46 @@
+import json
+import signal
+
+from src.govee_screen_sync.game_sync import game_loop
+from src.govee_screen_sync.light_device import GoveeLightDevice, get_device_location_indices
+
+
+def get_my_devices() -> list[GoveeLightDevice]:
+ try:
+ with open("devices.json", "r") as file:
+ devices_data = json.load(file)
+ except FileNotFoundError:
+ print("devices.json not found.")
+ print(
+ "Please make a copy of devices_template.json, rename it to devices.json, and configure your devices."
+ )
+ return []
+
+ devices = []
+ for device_data in devices_data:
+ ip = device_data["ip"]
+ name = device_data["name"]
+ screen_positions = get_device_location_indices(device_data["screen_positions"])
+ device = GoveeLightDevice(ip, name, screen_positions)
+ devices.append(device)
+
+ print(f"Devices: {[device.name for device in devices]}")
+ return devices
+
+
+def run():
+ devices = get_my_devices()
+
+ def power_off_devices_and_exit():
+ for device in devices:
+ device.power_off()
+ exit()
+
+ print("Starting Govee Light Controller...")
+ signal.signal(signal.SIGTERM, lambda signum, frame: power_off_devices_and_exit())
+ signal.signal(signal.SIGINT, lambda signum, frame: power_off_devices_and_exit())
+ game_loop(devices=devices)
+
+
+if __name__ == "__main__":
+ run()
diff --git a/src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc b/src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc
index b6978e49bf92ccbf34b4ae62cbad419b1e7bda48..fbd511a06c1f111531b13c6dede3f9bb376ed629 100644
GIT binary patch
delta 293
zcmZ1~H&>4LG%qg~0}!-J$fW6ST$j?mD5ZT_
zOlM8;29xW?E*FhmF6+Bq5_7$fkl4X}hl6)AGuu5z*~yRCnpk;Rg+I$}&Sd|^BB}&5
zw|FTd149GD9TA1eeht1Km?S4x@I)}OO}@xuSO37mK;ovZ$%?=Yj2jH6xXy4~Ah;m$nux{+kn9AmPaydZYz%_p
z*99~$3TUp-x+-A$ftQWf>;oSIuhexOt&2QbS9o+j2s7{t|NO!QRQ=-%4+D?vCkDpN
m6M0J+<;5A*J~9A_4<^!#@)Jrf$mm=Y*Zsl(ZeKQKv6F6N0~WS@M7$FBYZ8-t3;b!Cf-$`(5kFDP4F
zR(80q?0!+%{S5PEWuNb?%yL2>7=Xk>3Hcd}GYn@Weqm-1m73wWKyX3gH4%*u%s{yb
zT%SNnK5#GyieDGdyeOc#LhGu4=?8vxUb7DZ47^g;d9*I_XkFpa`5?-`FZ}Zh7f|(&
sFMJF_~4G%qg~0}!-J$fW%g+Q_$zk#W}Kt&Gi+t(cB8`cGzJ)?z9y+N{OALy(bg
z^J5V=M#j#`T4E()AJv3d4K}3gFx{B_fdR<85Ss9XL1^6>$<{26)t7#R&d
OGJwbrDU(k~8vy{>5+l?A
delta 117
zcmexn{>z;2G%qg~0}vGR$)s@#Z{%CX$T(;6R>o!~PKL=AOvf3$CoyYn)??l#$SAb=
zsfZgRWB+6wu@adN8VshEADKm14Q|LOEij##{Q<;&z$x;DRb=xvu}#d3nVU1E{26%y
S7#R&dGJwbr8IzAl8vy{s^&+(Z
diff --git a/src/govee_screen_sync/__pycache__/screen_capture.cpython-312.pyc b/src/govee_screen_sync/__pycache__/screen_capture.cpython-312.pyc
index 8a26c3f481d25e5b80888ef1c8a5ddbdf45eed39..676a452c0734664216f5fc2827e3d91b7a02cc26 100644
GIT binary patch
delta 103
zcmew+@=JvGG%qg~0}yaZ$)vGw@@44fRsuEkVbw7HI*mr3J}s@C%Oh4B|mJ+GSv
zTr>^15*U0vF!Ewxt0er9G; SegmentData:
+ def _get_segment_color_data(self, list_of_colors: list[Color], gradient=True) -> SegmentData:
"""Returns the SegmentData object for the given list of colors.
The segment data is a base64 encoded string that represents the colors to be set.
diff --git a/src/govee_screen_sync/main.py b/src/govee_screen_sync/main.py
deleted file mode 100644
index 480c2b3..0000000
--- a/src/govee_screen_sync/main.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import signal
-
-from govee_screen_sync.game_sync import game_loop
-from govee_screen_sync.light_device import GoveeLightDevice, get_device_location_indices
-from govee_screen_sync.utils import color_device_by_ip, discover_devices
-
-
-def get_my_devices() -> list[GoveeLightDevice] | None:
- """Returns a list of GoveeLightDevice objects that represent the devices in the room.
- This function should be implemented by the user.
-
- If you don't know the IP addresses of your devices, return an empty list
- """
- left_column_device = GoveeLightDevice(
- "192.168.0.248", "Govee Light Right", get_device_location_indices([7, 8, 9])
- )
- right_column_device = GoveeLightDevice(
- "192.168.0.108",
- "Govee Light Left",
- get_device_location_indices([0, 1, 2]),
- )
-
- my_devices = [left_column_device, right_column_device]
- print(f"Devices: {[my_devices.name for my_devices in my_devices]}")
-
- return my_devices
- # return []
-
-
-def run():
- devices = get_my_devices()
- if not devices:
- color_device_by_ip()
- return
- discover_devices(expected_devices=devices)
-
- def power_off_devices_and_exit():
- for device in devices:
- device.power_off()
- exit()
-
- print("Starting Govee Light Controller...")
- signal.signal(signal.SIGTERM, lambda signum, frame: power_off_devices_and_exit())
- signal.signal(signal.SIGINT, lambda signum, frame: power_off_devices_and_exit())
- game_loop(devices=devices)
-
-
-if __name__ == "__main__":
- run()
diff --git a/src/govee_screen_sync/screen_capture.py b/src/govee_screen_sync/screen_capture.py
index f42b8ee..94452c3 100644
--- a/src/govee_screen_sync/screen_capture.py
+++ b/src/govee_screen_sync/screen_capture.py
@@ -68,10 +68,7 @@ def capture_screen_and_process_colors() -> np.ndarray:
if DEBUG:
saturated_image.save("debug_saturated_image.png")
-
- most_colorful_pixels_image = Image.fromarray(
- most_colorful_pixels.astype(np.uint8), "RGB"
- )
+ most_colorful_pixels_image = Image.fromarray(most_colorful_pixels.astype(np.uint8), "RGB")
most_colorful_pixels_image.save("debug_preview.png")
# raise an exception to stop the program and show the image
raise Exception("Debug mode enabled. Stopping program to show image.")
From 54ddd5a6c0022f1543dc6034615dd3ca6880b432 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sun, 14 Apr 2024 17:49:51 -0400
Subject: [PATCH 09/11] improved
---
.gitignore | 5 ++
README.md | 101 ++++++++++++++++++++++++++
devices_template.json | 52 ++++++++-----
discover_devices.py | 5 +-
main.py | 18 ++---
src/govee_screen_sync/config.py | 3 +
src/govee_screen_sync/game_sync.py | 9 ++-
src/govee_screen_sync/light_device.py | 39 ++--------
src/govee_screen_sync/models.py | 10 +++
9 files changed, 171 insertions(+), 71 deletions(-)
diff --git a/.gitignore b/.gitignore
index f798ea2..be0f256 100644
--- a/.gitignore
+++ b/.gitignore
@@ -167,3 +167,8 @@ cython_debug/
# macos
*.DS_Store
+
+*.pyc
+
+# devices.json
+devices.json
diff --git a/README.md b/README.md
index 5eaa666..bac3c81 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,107 @@ Press Enter to power off the devices.
Use the discovered IP addresses to update the `devices.json` file accordingly.
+## Mapping Devices to Screen Sections
+Using the `devices.json` file, you can map each Govee device to specific sections of your computer screen. This mapping allows you to sonchronize the lighting effects with different parts of the screen content.
+
+
+### Moniter Grid
+```
+ 0 1 2 3 4 5 6 7 8 9
+ -----------------------------------------
+ 0 | x x x x x x x x x x |
+ 1 | x x x x x x x x x x |
+ 2 | x x x x x x x x x x |
+ 3 | x x x x x x x x x x |
+ 4 | x x x x x x x x x x |
+ 5 | x x x x x x x x x x |
+ 6 | x x x x x x x x x x |
+ 7 | x x x x x x x x x x |
+ 8 | x x x x x x x x x x |
+ 9 | x x x x x x x x x x |
+ -----------------------------------------
+ ||||||||||||||
+ ||||||||||||||
+ ______________________
+```
+
+For example, suppose you have two verticle light devices.
+You want to map them to the left and right sides of the screen.
+
+```
+- Map the left device (0) to the left side of the screen
+- Map the right device (1) to the right side of the screen.
+
+
+Govee device 0 govee device 1
+ ___ ___
+ [ 9 ] [ 9 ]
+ [ 9 ] [ 9 ]
+ [ 8 ] [ 8 ]
+ [ 8 ] [ 8 ]
+ [ 7 ] [ 7 ]
+ [ 7 ] 0 1 2 3 4 5 6 7 8 9 [ 7 ]
+ [ 6 ] ----------------------------------------- [ 6 ]
+ [ 6 ] 0 | 0 0 0 x x x x 1 1 1 | [ 6 ]
+ [ 5 ] 1 | 0 0 0 x x x x 1 1 1 | [ 5 ]
+ [ 5 ] 2 | 0 0 0 x x x x 1 1 1 | [ 5 ]
+ [ 4 ] 3 | 0 0 0 x x x x 1 1 1 | [ 4 ]
+ [ 4 ] 4 | 0 0 0 x x x x 1 1 1 | [ 4 ]
+ [ 3 ] 5 | 0 0 0 x x x x 1 1 1 | [ 3 ]
+ [ 3 ] 6 | 0 0 0 x x x x 1 1 1 | [ 3 ]
+ [ 2 ] 7 | 0 0 0 x x x x 1 1 1 | [ 2 ]
+ [ 2 ] 8 | 0 0 0 x x x x 1 1 1 | [ 2 ]
+ [ 1 ] 9 | 0 0 0 x x x x 1 1 1 | [ 1 ]
+ [ 1 ] ----------------------------------------- [ 1 ]
+ [ 0 ] |||||||||||||| [ 0 ]
+ [ 0 ] |||||||||||||| [ 0 ] _
+( _____ ) ______________________ ( _____ )
+```
+
+**The order of the device locations is likely 0-9 from closest to power source to the tip of the device, but I'm just guessing this.**
+
+We will use a "mask" of the screen to map the devices. The mask will be a 2D array that represents the screen. The value (0-9) in a location indicates that the device should be mapped to that position, while -1 indicates that it should not be used by the device.
+
+Device 0 Mapping
+```
+screen_map = [
+ [9, 9, 9, -1, -1, -1, -1, -1, -1, -1],
+ [8, 8, 8, -1, -1, -1, -1, -1, -1, -1],
+ [7, 7, 7, -1, -1, -1, -1, -1, -1, -1],
+ [6, 6, 6, -1, -1, -1, -1, -1, -1, -1],
+ [5, 5, 5, -1, -1, -1, -1, -1, -1, -1],
+ [4, 4, 4, -1, -1, -1, -1, -1, -1, -1],
+ [3, 3, 3, -1, -1, -1, -1, -1, -1, -1],
+ [2, 2, 2, -1, -1, -1, -1, -1, -1, -1],
+ [1, 1, 1, -1, -1, -1, -1, -1, -1, -1],
+ [0, 0, 0, -1, -1, -1, -1, -1, -1, -1],
+]
+```
+
+
+We can formalize this within our JSON object. You can provide either the screen positions or the screen mask. The screen mask is a 2D array that represents the screen. The value of 1 indicates that the device should be mapped to that position, while 0 indicates that it should not.
+```json
+[
+ {
+ "ip": "192.168.0.xxx",
+ "name": "device_0",
+ "screen_map": [
+ [9, 9, 9, -1, -1, -1, -1, -1, -1, -1],
+ [8, 8, 8, -1, -1, -1, -1, -1, -1, -1],
+ [7, 7, 7, -1, -1, -1, -1, -1, -1, -1],
+ [6, 6, 6, -1, -1, -1, -1, -1, -1, -1],
+ [5, 5, 5, -1, -1, -1, -1, -1, -1, -1],
+ [4, 4, 4, -1, -1, -1, -1, -1, -1, -1],
+ [3, 3, 3, -1, -1, -1, -1, -1, -1, -1],
+ [2, 2, 2, -1, -1, -1, -1, -1, -1, -1],
+ [1, 1, 1, -1, -1, -1, -1, -1, -1, -1],
+ [0, 0, 0, -1, -1, -1, -1, -1, -1, -1]
+ ]
+ }
+]
+```
+
+
## Configuration
1. Locate the `devices_template.json` file in the project directory.
diff --git a/devices_template.json b/devices_template.json
index 48ee2b4..b812042 100644
--- a/devices_template.json
+++ b/devices_template.json
@@ -1,20 +1,34 @@
[
- {
- "ip": "192.168.0.xxx",
- "name": "Govee Light 1",
- "screen_positions": [
- 0,
- 1,
- 2
- ]
- },
- {
- "ip": "192.168.0.yyy",
- "name": "Govee Light 2",
- "screen_positions": [
- 7,
- 8,
- 9
- ]
- }
-]
\ No newline at end of file
+ {
+ "ip": "192.168.0.xxx",
+ "name": "Govee Light 1",
+ "screen_map": [
+ [9, 9, 9, -1, -1, -1, -1, -1, -1, -1],
+ [8, 8, 8, -1, -1, -1, -1, -1, -1, -1],
+ [7, 7, 7, -1, -1, -1, -1, -1, -1, -1],
+ [6, 6, 6, -1, -1, -1, -1, -1, -1, -1],
+ [5, 5, 5, -1, -1, -1, -1, -1, -1, -1],
+ [4, 4, 4, -1, -1, -1, -1, -1, -1, -1],
+ [3, 3, 3, -1, -1, -1, -1, -1, -1, -1],
+ [2, 2, 2, -1, -1, -1, -1, -1, -1, -1],
+ [1, 1, 1, -1, -1, -1, -1, -1, -1, -1],
+ [0, 0, 0, -1, -1, -1, -1, -1, -1, -1]
+ ]
+ },
+ {
+ "ip": "192.168.0.yyy",
+ "name": "Govee Light 2",
+ "screen_map": [
+ [-1, -1, -1, -1, -1, -1, -1, 9, 9, 9],
+ [-1, -1, -1, -1, -1, -1, -1, 8, 8, 8],
+ [-1, -1, -1, -1, -1, -1, -1, 7, 7, 7],
+ [-1, -1, -1, -1, -1, -1, -1, 6, 6, 6],
+ [-1, -1, -1, -1, -1, -1, -1, 5, 5, 5],
+ [-1, -1, -1, -1, -1, -1, -1, 4, 4, 4],
+ [-1, -1, -1, -1, -1, -1, -1, 3, 3, 3],
+ [-1, -1, -1, -1, -1, -1, -1, 2, 2, 2],
+ [-1, -1, -1, -1, -1, -1, -1, 1, 1, 1],
+ [-1, -1, -1, -1, -1, -1, -1, 0, 0, 0]
+ ]
+ }
+]
diff --git a/discover_devices.py b/discover_devices.py
index 10a0823..71c5454 100644
--- a/discover_devices.py
+++ b/discover_devices.py
@@ -3,7 +3,7 @@
import time
from src.govee_screen_sync.light_device import GoveeLightDevice
-from src.govee_screen_sync.models import Color, DeviceScanMessage
+from src.govee_screen_sync.models import Color, DeviceScanMessage, NetworkedGoveeDevice
def discover_devices(
@@ -58,7 +58,8 @@ def discover_devices(
def color_device_by_ip():
# For each discovered device, create a GoveeLightDevice object
devices = [
- GoveeLightDevice(ip, f"Govee Light {i}", []) for i, ip in enumerate(discover_devices())
+ GoveeLightDevice(NetworkedGoveeDevice(ip=ip, name=f"Govee Light {i}", screen_map=[]))
+ for i, ip in enumerate(discover_devices())
]
rainbow_rgb = {
diff --git a/main.py b/main.py
index 18a9ce4..5667fef 100644
--- a/main.py
+++ b/main.py
@@ -1,30 +1,22 @@
-import json
import signal
from src.govee_screen_sync.game_sync import game_loop
-from src.govee_screen_sync.light_device import GoveeLightDevice, get_device_location_indices
+from src.govee_screen_sync.light_device import GoveeLightDevice
+from src.govee_screen_sync.models import NetworkedGoveeDevices
def get_my_devices() -> list[GoveeLightDevice]:
try:
with open("devices.json", "r") as file:
- devices_data = json.load(file)
+ my_devices = NetworkedGoveeDevices.model_validate_json(file.read())
except FileNotFoundError:
print("devices.json not found.")
print(
"Please make a copy of devices_template.json, rename it to devices.json, and configure your devices."
)
return []
-
- devices = []
- for device_data in devices_data:
- ip = device_data["ip"]
- name = device_data["name"]
- screen_positions = get_device_location_indices(device_data["screen_positions"])
- device = GoveeLightDevice(ip, name, screen_positions)
- devices.append(device)
-
- print(f"Devices: {[device.name for device in devices]}")
+ devices = [GoveeLightDevice(device) for device in my_devices.devices]
+ print(f"Found devices: {[device.name for device in devices]}")
return devices
diff --git a/src/govee_screen_sync/config.py b/src/govee_screen_sync/config.py
index 770ed96..2ea49f0 100644
--- a/src/govee_screen_sync/config.py
+++ b/src/govee_screen_sync/config.py
@@ -1,3 +1,6 @@
DEBUG = False
UDP_PORT = 4003
+
+# This is the number if different colors that can be displayed on the LED strip.
+# We use this to section the screen.
MAX_LED_COLOR_GRADIENT = 10
diff --git a/src/govee_screen_sync/game_sync.py b/src/govee_screen_sync/game_sync.py
index 10f855c..35fda20 100644
--- a/src/govee_screen_sync/game_sync.py
+++ b/src/govee_screen_sync/game_sync.py
@@ -1,5 +1,7 @@
import time
+import numpy as np
+
from govee_screen_sync.light_device import GoveeLightDevice
from govee_screen_sync.models import Color
from govee_screen_sync.screen_capture import capture_screen_and_process_colors
@@ -44,10 +46,11 @@ def game_loop(devices: list[GoveeLightDevice]):
while True:
screen_colors = capture_screen_and_process_colors()
for device in devices:
- selected_rows = screen_colors[device.screen_positions[0], :, :]
- selected_columns = selected_rows[:, device.screen_positions[1], :]
color_data = [
- Color(r=r, g=g, b=b) for r, g, b in selected_columns.mean(axis=1).astype(int)
+ Color.from_rgb(
+ screen_colors[(np.array(device.screen_map)) == i].mean(axis=0).astype(int)
+ )
+ for i in range(10)
]
device.set_segment_colors(color_data)
frame_counter.update_and_print()
diff --git a/src/govee_screen_sync/light_device.py b/src/govee_screen_sync/light_device.py
index 19360d7..d836776 100644
--- a/src/govee_screen_sync/light_device.py
+++ b/src/govee_screen_sync/light_device.py
@@ -11,6 +11,7 @@
ColorData,
Command,
Message,
+ NetworkedGoveeDevice,
PowerCommand,
PowerData,
PowerState,
@@ -20,10 +21,10 @@
class GoveeLightDevice:
- def __init__(self, ip: str, name: str, screen_positions: list[tuple[int, int]]):
- self.ip = ip
- self.name = name
- self.screen_positions = screen_positions
+ def __init__(self, device: NetworkedGoveeDevice):
+ self.ip = device.ip
+ self.name = device.name
+ self.screen_map = device.screen_map
def _send_command(self, command: Command, sleep_time=0.1):
message = Message(msg=command).model_dump_json()
@@ -98,33 +99,3 @@ def _get_segment_color_data(self, list_of_colors: list[Color], gradient=True) ->
byte_array.append(num2)
final_send_value = base64.b64encode(bytes(byte_array)).decode()
return SegmentData(pt=final_send_value)
-
-
-def get_device_location_indices(
- column_indices: list[int] | None = None, row_indices: list[int] | None = None
-):
- """
- screen example for MAX_LED_COLOR_GRADIENT = 10
-
- column numbers
- 0 1 2 3 4 5 6 7 8 9
- 0 x x x x x x x x x x
- 1 x x x x x x x x x x
- 2 x x x x x x x x x x
- 3 x x x x x x x x x x
- 4 x x x x x x x x x x
- 5 x x x x x x x x x x
- 6 x x x x x x x x x x
- 7 x x x x x x x x x x
- 8 x x x x x x x x x x
- 9 x x x x x x x x x x
-
- My device has index 0 at the bottom, so I need to reverse the row indices.
- """
- if column_indices is None:
- column_indices = list(range(MAX_LED_COLOR_GRADIENT))
- if row_indices is None:
- # light lamp colors are ordered from bottom to top
- row_indices = list(range(MAX_LED_COLOR_GRADIENT)[::-1])
- screen_indices = [row_indices, column_indices]
- return screen_indices
diff --git a/src/govee_screen_sync/models.py b/src/govee_screen_sync/models.py
index 9a39b2e..886c5f1 100644
--- a/src/govee_screen_sync/models.py
+++ b/src/govee_screen_sync/models.py
@@ -80,3 +80,13 @@ class DeviceScanCommand(Command):
class DeviceScanMessage(Message):
msg: DeviceScanCommand = DeviceScanCommand()
+
+
+class NetworkedGoveeDevice(BaseModel):
+ ip: str
+ name: str
+ screen_map: list[list[int]]
+
+
+class NetworkedGoveeDevices(BaseModel):
+ devices: list[NetworkedGoveeDevice]
From ba80cf4fc423332f93cc1417e7f192b2a84aaabd Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sun, 14 Apr 2024 20:05:15 -0400
Subject: [PATCH 10/11] remove files from git
---
devices.json | 20 ------------------
.../__pycache__/__init__.cpython-312.pyc | Bin 168 -> 0 bytes
.../__pycache__/config.cpython-312.pyc | Bin 243 -> 0 bytes
.../__pycache__/game_sync.cpython-312.pyc | Bin 3869 -> 0 bytes
.../__pycache__/light_device.cpython-312.pyc | Bin 7158 -> 0 bytes
.../__pycache__/models.cpython-312.pyc | Bin 4365 -> 0 bytes
.../screen_capture.cpython-312.pyc | Bin 2682 -> 0 bytes
.../__pycache__/utils.cpython-312.pyc | Bin 5451 -> 0 bytes
8 files changed, 20 deletions(-)
delete mode 100644 devices.json
delete mode 100644 src/govee_screen_sync/__pycache__/__init__.cpython-312.pyc
delete mode 100644 src/govee_screen_sync/__pycache__/config.cpython-312.pyc
delete mode 100644 src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc
delete mode 100644 src/govee_screen_sync/__pycache__/light_device.cpython-312.pyc
delete mode 100644 src/govee_screen_sync/__pycache__/models.cpython-312.pyc
delete mode 100644 src/govee_screen_sync/__pycache__/screen_capture.cpython-312.pyc
delete mode 100644 src/govee_screen_sync/__pycache__/utils.cpython-312.pyc
diff --git a/devices.json b/devices.json
deleted file mode 100644
index b6a0441..0000000
--- a/devices.json
+++ /dev/null
@@ -1,20 +0,0 @@
-[
- {
- "ip": "192.168.0.248",
- "name": "Govee Light Right",
- "screen_positions": [
- 7,
- 8,
- 9
- ]
- },
- {
- "ip": "192.168.0.108",
- "name": "Govee Light Left",
- "screen_positions": [
- 0,
- 1,
- 2
- ]
- }
-]
\ No newline at end of file
diff --git a/src/govee_screen_sync/__pycache__/__init__.cpython-312.pyc b/src/govee_screen_sync/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index e4c9476968b86c96b60d380f5fc82e41667fb37b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 168
zcmX@j%ge<81Y#eg(?IlN5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a(A|h2`x@7Dvrrc
zOw7!Vami0E%}vcKDUNZ^Eb%B!igC{`OHB=~%u9|*2eIOdlZ#SQ^Wuv^BJuH=d6^~g
l@p=W7zc_4i^HWN5QtgUZf#xy-aWRPTk(rT^v4|PS0s!L`572q
zg7o`oGT&l#adirHzr_*i5)dEY9~5#+%-1m@-pAD?-r3*BKPcWk$kD~q)h}cv!)K7W
zzg(TIVnT~ki;82i6B9GDV_fo+OLJ56N{VCLGfO;5lVaTS%TiN=EAx_L(m|~F;^d;#
z)V%m&kVtZVURq|lUP0wA4x8Nkl+v73yCP1YsURm8ivo!c%#4hTH#m43SZ{FiH*nmL
Nl)lIyR>TgJ0RWjwLMs3O
diff --git a/src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc b/src/govee_screen_sync/__pycache__/game_sync.cpython-312.pyc
deleted file mode 100644
index fbd511a06c1f111531b13c6dede3f9bb376ed629..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 3869
zcmc&1TWl29_0DTOyUg0_2Nw;h6eT}9V6xk(BSotE!QTd`lJY~(nb}!Cpb~07
zdS#zE_nvdlJ@?%6+AsZnAA#}P&7rC7A|Zdri97fz^7yBKTqP<|nFNXAmrXE9Hp()H
za|tfVM|m6P6GGAx^)Q4bhlna%BC2PRb)-b=)H+Sn*fy9>v8s59k9u=V#QPA*;}OOR
z97>O+mYCAOBB1
zl5(hHj$abnmV+HPBiAE0`sVn{4a-})Zl1Y*=El$G_y=O}F`8+O&2hhN^Up#1tHh|i
z#|*Nr8!J|R(HrE!w`ZA`l_1wndf6Q_ZiKAT%03f3t-$0r!0B9p%y?FjtEkGVB#7Qr
zd+8VR&7d{bjRLn=)sz7VFOp9C3^>~vFXnJ182*>z!@Hyym7C>_26ut$>^HuL`wi;M%Lr07(fgni^H(mpg0;}jOhYw3*(@8VJnc$sEVOg
z6AwtvZ&~C0>&bYk{w08mWVyBdYVN(<`@g<*s?@XFgum9^bJD6XSQ3NF?VUGYzyA7-
zy$dZLb>8kY+lPzd=5l?*e7{-WF~^ld;ZkV83=J&A7Sv*BmmO7QQ7VaTrr1^z!=@O%
z)x0G3l#h&+jzsSriGC2fwzU-OGlPBiH}&7q?+%rAjhMSe?r$Hxw`sKawlcS2S@f6d
zH_pedbmbNq2Ez-zTZ!*xBMQl;W|;q7*9!n*J$pUSGvW0?l(i+6ebq6RWlS
zI06)?5X}FUA0gYZGh-sKjac!^QV
zMJ@%i8|d_VTLuTG+P%bDF{)khp-&*cft-hMio+gYmWQli&;P?dn`_Mup&-oE^3}>I
zDtCu>Z#GlQPAhZ+-_9N`*9f`dxl(sUjPcVX#-42zS>a6^&+>yWlC*6~sHj
zV%65~v6naTWY!B@o>>vJ-sp0!d<$Op46yOLU}I(mJ#gJ{?FT%IG3cUV{H%ylFOY6x
zY<8Kq5?ap?qurfDFICI?XGn^_K;GfbkPEJ7?5(~ey8XJbavo^ixsC|al{>qfD<{DY
z^!Rnd<%1i%!IK>5ZF|E1Q3ux=k-G7RDrjq`5sHrqm4jE_iR+M47-})o7BiU-9?w9r
zG2-b|SU1v8JJj%GE}VrXX)w}g2eTz+(ib!;r&CrvBp7*;VrQCyhSO@o{wJ;^;&~_^
z+BCLO6ywbbRJ+qmS_g)73OcoWM{o3SMIm{QLJ>6HoxOBD}kXkHGzaX<9t+*mo-^@w=gX3oS_`JW|z2oD?y9Y}{N6n$5pYJz^#_zp#qPX*f+5MAZ
zApFhn;nMJ!IXqT8d8#-(c7OPEX?VgMo>)5j_WfbyJC5yWst^Ew9T-|TahJKf^MlcA
z`)@VgQod?=wSv@nA6yEUJfyL;6o{CC$U^Ufzz-{aP^?l<{K1mcYf8OKQh%ixbpO{A
zA8>#FL?RMcgAw|n9vAC-zY_c4enS7#ve@?x+KKaaRH#$
zB=ywh52TK7>wx|H`RV^=+f#ifJlEwP_a4|s{`^|of!)4O8@2=f>AsecjofEV>}ZgD
z*1{mZkpuiM!Ts#P5C;y2o?vx6_p@Ozfe|A+o<}i>TAQC|f_keZbSEmp&y!t$b$Ddr{a&C8orN?00C2`J^%pB$}oQ;8~#D|eocD6Cgb0ETA6L%
Y5&)HU2GHC6`nuaW5>>q6S7H6U=~;!^AiX^0a6GHBtRepmIQ6rg1U}pY#clu@0~Fr
zkt3lMRfD?HZmUWg_D4&f3ff3zB_4vh(z1O*>WiZ|BAW8BQn#vj3uGnKeQD3RoqK)nx#xW6+{<5lJ`V%wS50eU|MWA=->_mQXQ8lk2MQlBG9$A|CS>Jo
zh^2Kd#6iv_c~uArsw3o3ogpWSJbconiXl;Thuo?sFDV_df|lGc;?8@!u`EH;bUj}&z=vTIDe$4uXkX`6fg9g3!giC
zekkZQ>yDB{{IZc!bp4p7s*#jzR+pN3B1Xh?9Mh5-F}*aincTES`(1XM>kP~ii7Te}
zoHnWuyV*@^l+{CvgGR(q%<4fUt|}?RW~(aHkuhqCz@qL#W$8mGe84CSo&ZRnRd|_$
zk5>f6p*R(1j6*r$7juFvg^){bkR6H`<76ku$Su2WFd+}fiwRZ9Zp91j9^mrHRXA4m
zLb+P@L2r#*4YXEXBiF#(UOFQu*TNiib%~2TYdDGGQoLDa%2PE!KFb
z6?BAQ#+V=rM@|}`o2)_72qY+&g03WEE1GruNeR+-XODGVyr3&Yzj!4QNnE+uqeU}#
z82ZH%3FBmD_#)lO;8-epQ76%haokt9umc@ileo#SOn2Fy9y7&oIFU*i;qZ9#N+Gn{
zU9Z5e>rFteGYkHPN#R}BLPHZ|?geS*q%beGE&uK!(mT0QZD*Jqfu-dvCZFRhrFy;ENdSx!*m@v9_}0H9Wp(GXOEo@_W)R(
zflehQ8=C19(>R^O_gYihf*=a2jD
z?z?|7x8+dY+x>zEroUI!KJnJ&8aCY?`oo*QfAe53LD1UhiL)Ou1-D7PM?Xfb6PUP1p$Ha1<<
zhY0|w5gVLgUOQ!r_Q(P}-Q`g;99JXBjB=MX1sUKSP6@4npwg2XASQQqfNX_AkfY~k
z)~3;egte4?CORUXeO#j5TLEnBuGfHHM*@cw_{sEJUxdEA^x#r{`{{h3KPNUVh~CFy
z%dFUv3v8Viw~+vF{=bNfY=_0HmJxsU`0-^zsw?a@7ON;Fcd>iCWtBn~`+{Y1TH7|A
z_+sSC)Pq#M{Y-xCKu&Bflhd|&aXXch>4;)P5G^_(n4+j*=b=(r!?I;aU8Y5dR5*G?
zT8=wG)FQGUc*qVQ;P6oK2!?Q$NO;Tck;C-SEO(3`7N*Kn~qHSit?B`G%p4%ad?J{G1PxkCYGA;1kIH|Xt-Pw2cg0AmsVC;
z9M!nx64UtlRf@8lp#x^sUj+h!X>Gqh@TeufXDHu#At$yJC6fzmpBLNzgGGJ6ipXq9
zF{XLj@lc?tq$1@&BLp4e`}-gSNko!~aYdIR;DBg=i9Tppftn$WCXCBi0Jx`JmL}-!
zrkEK$a#Vk#_cTBhmo})94cz4kx?oa=1kvek0i9|}!)eI844nKJ2U&}-e`b8+DzUC+
z+6zChWa->DEp5|RWj%tZ-qSS7(#JY6`~
zNhAp7W_2>58(|Hx5v*Nb7EKn>>LGB#1&=L{CvCNha@V*kEij$iC?Xx!^))G$Wlq86
zuhJ-ptvEZbdj~yaMa2blE6fRTDcUYgS-wo^h41mMf-FoIFNxq`ok3S*2%P<(Vw9ba
zE@`o4=TSJXc;c#(l2A0rV2P&P1MoI7T#88rBR0GF-lvmO#zGu#*urU0QTXGbB9&>K7T-G6vecK@w&C!_Zh<
zEcJV^k_?H0zFImAp58f`ByN)(v!#=iF!5uYH~O^_lLu
zeW!Arr*l>P-#V~$QDoL_c-(qmw)MbF`=hqG)e??XTxsjx0m3dv8P6ym-Je
zP$!|Fvdl2?a)thSX#;39+=Dp+_xuzqa}#jSmpNIG9a+AT*~Oi3^kp>Fb^2zge&XJy2jy#FyVaHEif=^(Ns%QXPu=PjAA%E
zLSL6kTv#%SSHNuX`elzD!7zA5vSDOGG|&anloh9ozc3DWdD}
zLP7aD!HFXeE)d71GU_m3N(DT73c_KiJwE*tl%7gR?qSxN1D4q#SLGPPrujCD!%x4>IIF^jWO%ILsgEHzaQ^01kHU>^EY27_7S)l&dd$#VTyWQ`vIlOj@Aus7zuAM`&0mZD$71uW*gUlug}_bnm?_bK4Hh1-c&xdS?T@bAc0Z<9brll3UlFui23kcTfO;h(7+|Eldv%2ic)u
z13n_*TUj`4dct9NDaa(T>_UQXY5GkdH<(3^cXUkcUSuG<9i`dK
z(8Jdr@s9={iC^uQJ#>BvyPkPmj`g?STw)-5?&loapNpKMeG$l(smu}s8J2h4smJn;
z=OXXe_S^&HnWxUtb+6%`@_As1f#S2xe#g#x^32}PuPic9%w71!x0qAO(Y2Y?U~m~V
z1@CS$JT>ZO712iR%CdV1UJxj#z5@jSRpxqA2|_Eq0N9{p=M(TCd41iF8zZyu$NiGW
zca`FJ79TX@+ys~90JQkXJK!RzZCg)@l5!0^dJ+%-kf8FTwR8xg4?AA4HeRrMolYc4
zJHc)`;M)b?Zus`Vw->%n`1ZlKAHD;Wcc*j>%0Ku~-i{yU!?`H$u9x85{Sv%;UV?Y;
zOYnBS1n<6=;NAZcya#?HFBR~aG0A#y27iLL3t71a$6jzIR-m{IVmql%N-2tr;T=(~
z!XvlkHo!b^4BCq2SO%W{3lh8~jDW714iZVl6&k2P6F!s?4cn^=&)$WhI$9DOx%F@u
zz<|zLQ#h$bY4B^?ATfY54xio!1R|>~jM$iSwLWQX{aDFw>Y7{k)4Aq@lhq3@f6mqN
zO=HW)d-JdEn+xoJ&@i)WreQYFJ=b`6vTC96)!W^|Yrl{a5u5rhk^{f5G`!?ge9$
zC0ddOSa#<#2I->AL4KZn#s}CI_>Y9C`rq{37G@hZEz*Xr?iu|-@S!~0b?RGepo{o7
Dyp@uJ
diff --git a/src/govee_screen_sync/__pycache__/models.cpython-312.pyc b/src/govee_screen_sync/__pycache__/models.cpython-312.pyc
deleted file mode 100644
index 85c10276540399b47533ecb295e7426b3396418b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 4365
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
V_t{C33~mj7KlXI&6@huv$={>1Z;Su{
diff --git a/src/govee_screen_sync/__pycache__/utils.cpython-312.pyc b/src/govee_screen_sync/__pycache__/utils.cpython-312.pyc
deleted file mode 100644
index a056a2639297a21ce291c7e37fb8ef71b22d3d26..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 5451
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
Sc{h&10k8lY*DE>Fz8L&|R
From 1d2daec51f07919b9c6b4bc27ac5eb56343b8f40 Mon Sep 17 00:00:00 2001
From: Steve <108346431+StevenKauwe@users.noreply.github.com>
Date: Sun, 14 Apr 2024 20:12:20 -0400
Subject: [PATCH 11/11] reference to origin repo
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index bac3c81..69fbf18 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,10 @@
-Here's the updated README with the new device discovery section:
-
# Govee Screen Sync
Govee Screen Sync is a Python project that synchronizes Govee LED lights with your computer screen, creating an immersive lighting experience. The lights will change colors based on the content displayed on your screen.
+## Acknowledgement
+This project builds from the work of https://github.com/strunker/GoveeSync who wrote out all the commands and showed how to use the Govee API. They also found the 'razer' command which is the most importart part, low-latency control of the color segments.
+
## Prerequisites
Before getting started, ensure that you have the following: