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.
-
    -
  1. Replace line 10 with your device IP, for example: DeviceIP = "192.168.0.1"
  2. -
  3. Open cmd prompt then simply run with - python c:\PathToFile\GameSync2023.py
  4. -
  5. You can simply do Ctrl + C to exit when you want to end the light sync.
  6. -
- -# 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 -
    -
  1. 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
  2. -
  3. 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.
  4. -
- - 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.
+
    +
  1. Replace line 10 with your device IP, for example: DeviceIP = "192.168.0.1"
  2. +
  3. Open cmd prompt then simply run with - python c:\PathToFile\GameSync2023.py
  4. +
  5. You can simply do Ctrl + C to exit when you want to end the light sync.
  6. +
+ +# 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 +
    +
  1. 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
  2. +
  3. 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.
  4. +
+ + 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.
-
    -
  1. Replace line 10 with your device IP, for example: DeviceIP = "192.168.0.1"
  2. -
  3. Open cmd prompt then simply run with - python c:\PathToFile\GameSync2023.py
  4. -
  5. You can simply do Ctrl + C to exit when you want to end the light sync.
  6. -
- -# 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 -
    -
  1. 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
  2. -
  3. 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.
  4. -
- - 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 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 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: