Skip to content

Commit

Permalink
Relase 2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Galorhallen committed Jan 4, 2025
2 parents d95a696 + e5f1721 commit 931f2d5
Show file tree
Hide file tree
Showing 13 changed files with 1,819 additions and 631 deletions.
27 changes: 27 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: Bug report
about: A light is supported but one or more functionalities are missing
title: ''
labels: bug
assignees: Galorhallen

---

If you are brought here a by this log line in Home Assistant:
`Device %s is not supported. Only power control is available. Please open an issue at 'https://github.com/Galorhallen/govee-local-api/issues'` please open a feature request

### 💡 Bug
- **Model**:
- **Bugged functionalities**:
- [ ] RGB control is missing
- [ ] Brightness control is missing
- [ ] White Temperature control is missing
- [ ] Scenes are missing
- [ ] Segments

### 🔧 Bug Description
Describe the issue you’re experiencing with your Govee light.
If the bug is related to segments please specify the correct number of supported segments.
If the bug related to scenes a screenshot of the application scene view for the device could be useful.

---
22 changes: 22 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: Feature request
about: Request support for a light model
title: ''
labels: enhancement
assignees: Galorhallen

---

## 🚀 Light Support Template for Govee LAN API Library
---

### 💡 Light Details
Provide information about the Govee light(s) features:
- **Model**:
- **Supported Functionalities**:
- [ ] RGB
- [ ] Brightness
- [ ] White Temperature
- [ ] Scenes
- **Number of Segments**:
---
6 changes: 3 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -60,7 +60,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.12"
python-version: "3.13"
- uses: snok/install-poetry@v1
- name: Install dependencies
run: |
Expand All @@ -83,7 +83,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.12"
python-version: "3.13"
- uses: snok/install-poetry@v1
- name: Install dependencies
run: |
Expand Down
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
repos:
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 24.10.0
hooks:
- id: black
language_version: python3.12
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.8
rev: v0.7.0
hooks:
- id: ruff
types_or: ["python"]
args: ["--fix"]
- id: ruff-format
files: ^.+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.2.2
rev: v2.3.0
hooks:
- id: codespell
args:
- --quiet-level=2
exclude_types: [csv, json]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v5.0.0
hooks:
- id: check-executables-have-shebangs
stages: [manual]
- repo: https://github.com/cdce8p/python-typing-update
rev: v0.6.0
rev: v0.7.0
hooks:
# Run `python-typing-update` hook manually from time to time
# to update python typing syntax.
Expand All @@ -43,6 +43,6 @@ repos:
hooks:
- id: prettier
- repo: "https://github.com/pre-commit/mirrors-mypy"
rev: "v1.8.0"
rev: "v1.12.0"
hooks:
- id: mypy
236 changes: 200 additions & 36 deletions example/main.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,220 @@
import asyncio
from prompt_toolkit import PromptSession
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.shortcuts import clear

from govee_local_api import GoveeController, GoveeDevice
from govee_local_api import GoveeController, GoveeDevice, GoveeLightFeatures


def update_device_callback(device: GoveeDevice):
print(f"Goveee device update callback: {device}")
def update_device_callback(device: GoveeDevice) -> None:
# print(f"Goveee device update callback: {device}")
pass


def discovered_callback(device: GoveeDevice, is_new: bool) -> bool:
if is_new:
print(f"Discovered: {device}. New: {is_new}")
# print(f"Discovered: {device}. New: {is_new}")
device.set_update_callback(update_device_callback)
return True


async def print_status(controller: GoveeController):
async def create_controller() -> GoveeController:
controller = GoveeController(
loop=asyncio.get_event_loop(),
listening_address="0.0.0.0",
discovery_enabled=True,
discovered_callback=discovered_callback,
evicted_callback=lambda device: print(f"Evicted {device}"),
)
await controller.start()
while not controller.devices:
print("Waiting for devices... ")
await asyncio.sleep(1)
print("Devices: ", [str(d) for d in controller.devices])
return controller


async def menu(device: GoveeDevice) -> None:
print("\nDevice: ", device)
print("Select an option:")
print("0. Exit")
print("1. Turn on")
print("2. Turn off")
print("3. Set brightness (0-100)")
print("4. Set color (R G B)")
print("5. Set segment color (Segment, R G B)")
print("6. Set scene")
print("7. Send raw hex")
print("8. Clear screen")
print("9. Clear Device")


async def handle_turn_on(device: GoveeDevice) -> None:
print("Turning on the LED strip...")
await device.turn_on()


async def handle_turn_off(device: GoveeDevice) -> None:
print("Turning off the LED strip...")
await device.turn_off()


async def handle_set_brightness(device: GoveeDevice, session: PromptSession) -> None:
while True:
if not controller.devices:
print("No devices found")
for device in controller.devices:
print(f"Status: {device}")
await asyncio.sleep(5)
brightness = await session.prompt_async("Enter brightness (0-100): ")
try:
brightness = int(brightness)
if 0 <= brightness <= 100:
print(f"Setting brightness to {brightness}%")
await device.set_brightness(brightness)
break
else:
print("Please enter a value between 0 and 100.")
except ValueError:
print("Invalid input, please enter an integer.")


async def main(controller: GoveeController):
await controller.start()
print("start")
await asyncio.sleep(5)
print("Waited")
async def handle_set_color(device: GoveeDevice, session: PromptSession) -> None:
while True:
color = await session.prompt_async("Enter color (R G B, values 0-255): ")
try:
r, g, b = map(int, color.split())
if all(0 <= v <= 255 for v in [r, g, b]):
print(f"Setting color to RGB({r}, {g}, {b})")
await device.set_rgb_color(r, g, b)
break
else:
print("RGB values must be between 0 and 255.")
except ValueError:
print("Invalid input, please enter three integers separated by spaces.")

# device = controller.get_device_by_ip("10.0.0.183")
# await device.turn_on()
# await asyncio.sleep(5)
await print_status(controller)

async def handle_set_segment_color(device: GoveeDevice, session: PromptSession) -> None:
if device.capabilities.features & GoveeLightFeatures.SEGMENT_CONTROL == 0:
print("This device does not support segment control.")
return
while True:
segment = await session.prompt_async(
f"Enter segment number (1-{device.capabilities.segments_count}): "
)
try:
segment = int(segment)
if 1 <= segment <= device.capabilities.segments_count:
color = await session.prompt_async(
"Enter segment color (R G B, values 0-255): "
)
r, g, b = map(int, color.split())
if all(0 <= v <= 255 for v in [r, g, b]):
print(f"Setting segment {segment} color to RGB({r}, {g}, {b})")
await device.set_segment_rgb_color(segment, r, g, b)
break
else:
print("RGB values must be between 0 and 255.")
else:
print(
f"Segment number must be between 1 and {device.capabilities.segments_count}."
)
except ValueError:
print(
"Invalid input. Please enter an integer for the segment and three integers for the color."
)

if __name__ == "__main__":
loop = asyncio.new_event_loop()
controller: GoveeController = GoveeController(
loop=loop,
listening_address="0.0.0.0",
discovery_enabled=True,
discovered_callback=discovered_callback,
evicted_callback=lambda device: print(f"Evicted {device}"),
)

async def handle_set_scene(device: GoveeDevice, session: PromptSession):
while True:
scenes = device.capabilities.available_scenes
for idx, scene in enumerate(scenes):
print(f"{idx}: {scene}")

scene = await session.prompt_async(f"Enter scene number (0-{len(scenes)}): ")
scene_name = scenes[int(scene)]
if scene_name in device.capabilities.available_scenes:
print(f"Setting scene to {scene}")
await device.set_scene(scene_name)
break
else:
print("Invalid scene. Please choose from the available scenes.")


async def handle_clear_screen(device):
clear()


async def handle_exit(device):
print("Exiting...")
raise SystemExit()


async def devices_menu(session, devices: list[GoveeDevice]) -> GoveeDevice:
selection = None
devices = sorted(devices, key=lambda d: d.sku)
while (
selection is None
or selection == ""
or not selection.isnumeric()
or int(selection) >= len(devices)
):
for idx, device in enumerate(devices):
print(f"{idx}: {device.ip} - {device.sku}")
selection = await session.prompt_async(
f"Choose an option (0-{len(devices) - 1}): "
)
if not selection.isnumeric():
continue
print(f"Selected device: {selection}")
return devices[int(selection)]


async def handle_send_hex(device: GoveeDevice, session: PromptSession) -> None:
hex_data = await session.prompt_async("Enter hex data: ")
await device.send_raw_command(hex_data)


async def repl() -> None:
session = PromptSession()
print("Welcome to the LED Control REPL.")

# Dictionary of command handlers
command_handlers = {
"0": handle_exit,
"1": handle_turn_on,
"2": handle_turn_off,
"3": lambda device: handle_set_brightness(device, session),
"4": lambda device: handle_set_color(device, session),
"5": lambda device: handle_set_segment_color(device, session),
"6": lambda device: handle_set_scene(device, session),
"7": lambda device: handle_send_hex(device, session),
"8": handle_clear_screen,
}

controller: GoveeController = await create_controller()
selected_device: GoveeDevice | None = None

while True:
if not selected_device:
selected_device = await devices_menu(session, controller.devices)

await menu(selected_device)
with patch_stdout():
user_choice = await session.prompt_async("Choose an option (1-10): ")
if user_choice == "9":
selected_device = None
continue

# Get the command handler from the dictionary, or return an invalid message if not found
handler = command_handlers.get(user_choice)
if handler:
await handler(selected_device) # Call the appropriate command handler
else:
print("Invalid option, please try again.")


async def main():
await repl()


if __name__ == "__main__":
try:
loop.run_until_complete(main(controller))
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
print("Closing Loop")
controller.cleanup()
loop.close()
asyncio.run(main())
except (EOFError, KeyboardInterrupt):
print("REPL exited.")
Loading

0 comments on commit 931f2d5

Please sign in to comment.