diff --git a/.github/update-readme.py b/.github/update-readme.py new file mode 100644 index 00000000..ebeb9401 --- /dev/null +++ b/.github/update-readme.py @@ -0,0 +1,195 @@ +# Copyright (c) 2023, Bas Nijholt +# All rights reserved. +# When using this code, please cite the original source. +# and include the LICENSE file in your project. +"""Automatically update Markdown files with code block output. + +Add code blocks between and in your Markdown file. +The output will be inserted between and . + +Example: +------- +``` + + + + +This will be replaced by the output of the code block above. + + +``` +""" +from __future__ import annotations + +import contextlib +import io +from pathlib import Path + + +def md_comment(text: str) -> str: + """Format a string as a Markdown comment.""" + return f"" + + +MARKERS = { + "warning": md_comment("THIS CONTENT IS AUTOMATICALLY GENERATED"), + "start_code": md_comment("START_CODE"), + "end_code": md_comment("END_CODE"), + "start_output": md_comment("START_OUTPUT"), + "end_output": md_comment("END_OUTPUT"), +} + + +def remove_md_comment(commented_text: str) -> str: + """Remove Markdown comment tags from a string.""" + if not (commented_text.startswith("")): + raise ValueError("Invalid Markdown comment format") + return commented_text[5:-4] + + +def execute_code_block(code: list[str]) -> list[str]: + """Execute a code block and return its output as a list of strings.""" + f = io.StringIO() + with contextlib.redirect_stdout(f): + exec("\n".join(code)) # noqa: S102 + return f.getvalue().split("\n") + + +def process_markdown(content: list[str]) -> list[str]: + """Executes code blocks in a list of Markdown-formatted strings and returns the modified list. + + Parameters + ---------- + content + A list of Markdown-formatted strings. + + Returns + ------- + list[str] + A modified list of Markdown-formatted strings with code block output inserted. + """ + assert isinstance(content, list), "Input must be a list" + new_lines = [] + code = [] + in_code_block = in_output_block = False + output = None + + for line in content: + if MARKERS["start_code"] in line: + in_code_block = True + elif MARKERS["start_output"] in line: + in_output_block = True + new_lines.extend([line, MARKERS["warning"]] + output) + output = None + elif MARKERS["end_output"] in line: + in_output_block = False + elif in_code_block: + if MARKERS["end_code"] in line: + in_code_block = False + output = execute_code_block(code) + code = [] + else: + code.append(remove_md_comment(line)) + + if not in_output_block: + new_lines.append(line) + + return new_lines + + +def update_markdown_file(filepath: Path) -> None: + """Rewrite a Markdown file by executing and updating code blocks.""" + with filepath.open() as f: + original_lines = [line.rstrip("\n") for line in f.readlines()] + + new_lines = process_markdown(original_lines) + updated_content = "\n".join(new_lines).rstrip() + "\n" + + with filepath.open("w") as f: + f.write(updated_content) + + +def test_process_markdown(): + def assert_process(input_lines, expected_output): + output = process_markdown(input_lines) + assert output == expected_output, f"Expected {expected_output}, got {output}" + + # Test case 1: Single code block + input_lines = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + "This content will be replaced", + MARKERS["end_output"], + "More text", + ] + expected_output = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + MARKERS["warning"], + "Hello, world!", + "", + MARKERS["end_output"], + "More text", + ] + assert_process(input_lines, expected_output) + + # Test case 2: Two code blocks + input_lines = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + "This content will be replaced", + MARKERS["end_output"], + "More text", + MARKERS["start_code"], + md_comment("print('Hello again!')"), + MARKERS["end_code"], + MARKERS["start_output"], + "This content will also be replaced", + MARKERS["end_output"], + ] + expected_output = [ + "Some text", + MARKERS["start_code"], + md_comment("print('Hello, world!')"), + MARKERS["end_code"], + MARKERS["start_output"], + MARKERS["warning"], + "Hello, world!", + "", + MARKERS["end_output"], + "More text", + MARKERS["start_code"], + md_comment("print('Hello again!')"), + MARKERS["end_code"], + MARKERS["start_output"], + MARKERS["warning"], + "Hello again!", + "", + MARKERS["end_output"], + ] + assert_process(input_lines, expected_output) + + # Test case 3: No code blocks + input_lines = [ + "Some text", + "More text", + ] + expected_output = [ + "Some text", + "More text", + ] + assert_process(input_lines, expected_output) + + +if __name__ == "__main__": + test_process_markdown() + update_markdown_file(Path(__file__).parent.parent / "README.md") diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index cf1866fb..7326175a 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -9,6 +9,11 @@ on: jobs: docker: runs-on: ubuntu-latest + strategy: + matrix: + platform: + - linux/amd64 + - linux/arm64 steps: - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -24,6 +29,5 @@ jobs: with: # Only push on the master branch push: ${{ github.ref == 'refs/heads/master' }} - # TODO: fix builds on linux/arm/v7 - platforms: linux/amd64,linux/arm64 + platforms: ${{ matrix.platform }} tags: ${{ secrets.DOCKERHUB_USERNAME }}/adaptive-lighting:latest diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml new file mode 100644 index 00000000..75820072 --- /dev/null +++ b/.github/workflows/install_dependencies/action.yml @@ -0,0 +1,36 @@ +name: 'Install Dependencies' +description: 'Install Home Assistant and test dependencies' +inputs: + python_version: + description: 'Python version' + required: true + default: '3.10' + +runs: + using: "composite" + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3 + with: + repository: ${{ github.repository }} + ref: ${{ github.ref }} + persist-credentials: false + fetch-depth: 0 + - name: Check out code from GitHub + uses: actions/checkout@v3 + with: + repository: home-assistant/core + path: core + - name: Set up Python ${{ inputs.python_version }} + id: python + uses: actions/setup-python@v4.1.0 + with: + python-version: ${{ inputs.python_version }} + - name: Install dependencies + shell: bash + run: | + echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" + pip install -r core/requirements.txt --use-pep517 + pip install -r core/requirements_test.txt --use-pep517 + pip install -e core/ --use-pep517 + pip install $(python test_dependencies.py) --use-pep517 diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 60d9577f..47d4df8a 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -6,7 +6,6 @@ on: pull_request: jobs: - pytest: name: Run pytest runs-on: ubuntu-20.04 @@ -16,17 +15,13 @@ jobs: python-version: ["3.10"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 - - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3 + + - name: Install Home Assistant + uses: ./.github/workflows/install_dependencies with: - repository: home-assistant/core - path: core - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@v4.1.0 - with: - python-version: ${{ matrix.python-version }} + python_version: ${{ matrix.python-version }} + - name: Click here for troubleshooting steps if tests break again. run: | echo "::notice::### If tests fail, try these debug steps: ###" @@ -36,13 +31,6 @@ jobs: echo "::notice::### 4. ERROR:homeassistant.setup:Setup failed for 'component': Unable to import component: No module named ''module'' ###" echo "::notice::### 5. add 'component'.'module' (without the '') from the above log into the 'required' list inside of 'test_dependencies.py' ###" echo "::notice::### 6. Try again! If more issues persist they should be easily solvable by reading the verbose logs now. ###" - - name: Install dependencies - run: | - echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" - pip install -r core/requirements.txt --use-pep517 - pip install -r core/requirements_test.txt --use-pep517 - pip install -e core/ --use-pep517 - pip install $(python test_dependencies.py) --use-pep517 - name: Run pytest timeout-minutes: 60 run: | diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml new file mode 100644 index 00000000..36a994a9 --- /dev/null +++ b/.github/workflows/update-readme.yml @@ -0,0 +1,44 @@ +name: Update README.md + +on: + push: + branches: + - master + paths: + - ".github/update-readme.py" + - "README.md" + - ".github/workflows/update-readme.yml" + - "custom_components/adaptive_lighting/const.py" + pull_request: + +jobs: + update_readme: + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3 + + - name: Install Home Assistant + uses: ./.github/workflows/install_dependencies + with: + python_version: "3.10" + + - name: Install pandas and tabulate + run: | + pip install pandas tabulate + + - name: Run update-readme.py + run: python ./.github/update-readme.py + + - name: Commit updated README.md + run: | + git add README.md + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git diff --quiet && git diff --staged --quiet || git commit -m "Update README.md" + + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.head_ref }} diff --git a/README.md b/README.md index ac7897e0..7cad61a3 100644 --- a/README.md +++ b/README.md @@ -77,36 +77,50 @@ Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the ### :memo: Options -| Option | Description | Required | Default | Type | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | --------- | -| `name` | Display name for this switch. | ❌ | `default` | `string` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. | ❌ | `False` | `boolean` | -| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | ❌ | `list` | `list` | -| `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | ❌ | `False` | `boolean` | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | ❌ | `1` | `time` | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled. 😴 | ❌ | `1` | `time` | -| `transition` | Duration of transition when lights change, in seconds. | ❌ | `45` | `integer` | -| `interval` | Frequency to adapt the lights, in seconds. | ❌ | `90` | `integer` | -| `min_brightness` | Minimum brightness percentage. 💡 | ❌ | `1` | `integer` | -| `max_brightness` | Maximum brightness percentage. 💡 | ❌ | `100` | `integer` | -| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | ❌ | `2000` | `integer` | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | ❌ | `5500` | `integer` | -| `sleep_brightness` | Brightness of lights in sleep mode. 😴 | ❌ | `1` | `integer` | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. | ❌ | `'color_temp'` | `string` | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is `"rgb_color"`). 🌈 | ❌ | `[255, 56, 0]` | `list` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`). 😴 | ❌ | `1000` | `integer` | -| `sunrise_time` | Set a fixed time for sunrise. 🌅 | ❌ | `None` | `time` | -| `max_sunrise_time` | Set the latest virtual sunrise time, allowing for earlier real sunrises. 🌅 | ❌ | `None` | `time` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset. ⏰ | ❌ | `0` | `time` | -| `sunset_time` | Set a fixed time for sunset. 🌇 | ❌ | `None` | `time` | -| `min_sunset_time` | Set the earliest virtual sunset time, allowing for later real sunsets. 🌇 | ❌ | `None` | `time` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset. ⏰ | ❌ | `0` | `time` | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | ❌ | `False` | `boolean` | -| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | ❌ | `True` | `boolean` | -| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | ❌ | `False` | `boolean` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | ❌ | `False` | `boolean` | -| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | ❌ | `0` | `integer` | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | ❌ | `0` | `integer` | +All of the configuration options are listed below, along with their default values. +The YAML and frontend configuration methods support all of the options listed below. + + + + + + + + + + + +| Variable name | Description | Default | Type | +|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| +| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | `False` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | `1` | `float` 0-6553 | +| `sleep_transition` | Duration of transition when 'sleep mode' is toggled. 😴 | `1` | `float` 0-6553 | +| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | +| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | +| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `sleep_brightness` | Brightness of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`). 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). 🌈 | `[255, 56, 0]` | RGB color | +| `sunrise_time` | Set a fixed time for sunrise. 🌅 | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time, allowing for earlier real sunrises. 🌅 | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time for sunset. 🌇 | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time, allowing for later real sunsets. 🌇 | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset. ⏰ | `0` | `int` | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | +| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` | +| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | +| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | `0` | `float > 0` | + + Full example: diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index f4b751e5..421eadcc 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -14,44 +14,144 @@ DOMAIN = "adaptive_lighting" SUN_EVENT_NOON = "solar_noon" SUN_EVENT_MIDNIGHT = "solar_midnight" +DOCS = {} + + CONF_NAME, DEFAULT_NAME = "name", "default" +DOCS[CONF_NAME] = "Display name for this switch. 📝" + CONF_LIGHTS, DEFAULT_LIGHTS = "lights", [] +DOCS[CONF_LIGHTS] = ( + "List of light entities to be controlled by Adaptive " "Lighting (may be empty). 🌟" +) + CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = ( "detect_non_ha_changes", False, ) +DOCS[CONF_DETECT_NON_HA_CHANGES] = ( + "Detect non-`light.turn_on` state changes and stop adapting lights. " + "Requires `take_over_control`. 🕵️" +) + CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = ( "include_config_in_attributes", False, ) +DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = ( + "Show all options as attributes on the switch in " + "Home Assistant when set to `true`. 📝" +) + CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1 +DOCS[CONF_INITIAL_TRANSITION] = ( + "Duration of the first transition when lights turn " "from `off` to `on`. ⏲️" +) + CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1 +DOCS[CONF_SLEEP_TRANSITION] = "Duration of transition when 'sleep mode' is toggled. 😴" + CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90 +DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄" + CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100 +DOCS[CONF_MAX_BRIGHTNESS] = "Maximum brightness percentage. 💡" + CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500 +DOCS[CONF_MAX_COLOR_TEMP] = "Coldest color temperature in Kelvin. ❄️" + CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS = "min_brightness", 1 +DOCS[CONF_MIN_BRIGHTNESS] = "Minimum brightness percentage. 💡" + CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000 +DOCS[CONF_MIN_COLOR_TEMP] = "Warmest color temperature in Kelvin. 🔥" + CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False +DOCS[CONF_ONLY_ONCE] = ( + "Adapt lights only when they are turned on (`true`) or keep adapting them " + "(`false`). 🔄" +) + CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False +DOCS[ + CONF_PREFER_RGB_COLOR +] = "Use RGB color adjustment instead of native light color temperature. 🌈" + CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = ( "separate_turn_on_commands", False, ) +DOCS[CONF_SEPARATE_TURN_ON_COMMANDS] = ( + "Use separate `light.turn_on` calls for color and brightness, needed for " + "some light types. 🔀" +) + CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1 +DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness of lights in sleep mode. 😴" + CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000 +DOCS[CONF_SLEEP_COLOR_TEMP] = ( + "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is " + "`color_temp`). 😴" +) + CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0] +DOCS[CONF_SLEEP_RGB_COLOR] = ( + "RGB color in sleep mode (used when " "`sleep_rgb_or_color_temp` is 'rgb_color'). 🌈" +) + CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = ( "sleep_rgb_or_color_temp", "color_temp", ) +DOCS[ + CONF_SLEEP_RGB_OR_COLOR_TEMP +] = "Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙" + CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0 +DOCS[CONF_SUNRISE_OFFSET] = "Adjust sunrise time with a positive or negative offset. ⏰" + CONF_SUNRISE_TIME = "sunrise_time" +DOCS[CONF_SUNRISE_TIME] = "Set a fixed time for sunrise. 🌅" + CONF_MAX_SUNRISE_TIME = "max_sunrise_time" +DOCS[CONF_MAX_SUNRISE_TIME] = ( + "Set the latest virtual sunrise time, allowing" " for earlier real sunrises. 🌅" +) + CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 +DOCS[CONF_SUNSET_OFFSET] = "Adjust sunset time with a positive or negative offset. ⏰" + CONF_SUNSET_TIME = "sunset_time" +DOCS[CONF_SUNSET_TIME] = "Set a fixed time for sunset. 🌇" + CONF_MIN_SUNSET_TIME = "min_sunset_time" +DOCS[CONF_MIN_SUNSET_TIME] = ( + "Set the earliest virtual sunset time, allowing" " for later real sunsets. 🌇" +) + CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True +DOCS[CONF_TAKE_OVER_CONTROL] = ( + "Disable Adaptive Lighting if another source calls `light.turn_on` while lights " + "are on and being adapted. Note that this calls `homeassistant.update_entity` " + "every `interval`! 🔒" +) + CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 +DOCS[CONF_TRANSITION] = "Duration of transition when lights change, in seconds. 🕑" + +CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 +DOCS[CONF_ADAPT_DELAY] = ( + "Wait time (seconds) between light turn on and Adaptive Lighting applying " + "changes. Helps avoid flickering. ⏲️" +) + +CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 +DOCS[CONF_SEND_SPLIT_DELAY] = ( + "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. " + "Helps ensure correct handling. ⏲️" +) + SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" @@ -69,9 +169,8 @@ CONF_TURN_ON_LIGHTS = "turn_on_lights" SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" CONF_USE_DEFAULTS = "use_defaults" -CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 + TURNING_OFF_DELAY = 5 -CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0 def int_between(min_int, max_int): @@ -169,3 +268,57 @@ _DOMAIN_SCHEMA = vol.Schema( for key, default, validation in _yaml_validation_tuples } ) + + +def _format_voluptuous_instance(instance): + coerce_type = None + min_val = None + max_val = None + + for validator in instance.validators: + if isinstance(validator, vol.Coerce): + coerce_type = validator.type.__name__ + elif isinstance(validator, (vol.Clamp, vol.Range)): + min_val = validator.min + max_val = validator.max + + if min_val is not None and max_val is not None: + return f"`{coerce_type}` {min_val}-{max_val}" + elif min_val is not None: + return f"`{coerce_type} > {min_val}`" + elif max_val is not None: + return f"`{coerce_type} < {max_val}`" + else: + return f"`{coerce_type}`" + + +def generate_markdown_table(): + import pandas as pd + + rows = [] + for k, default, type_ in VALIDATION_TUPLES: + description = DOCS[k] + if type_ == cv.entity_ids: + type_ = "list of `entity_id`s" + elif type_ in (bool, int, float, str): + type_ = f"`{type_.__name__}`" + elif isinstance(type_, vol.All): + type_ = _format_voluptuous_instance(type_) + elif isinstance(type_, vol.In): + type_ = f"one of `{type_.container}`" + elif isinstance(type_, selector.SelectSelector): + type_ = f"one of `{type_.config['options']}`" + elif isinstance(type_, selector.ColorRGBSelector): + type_ = "RGB color" + else: + raise ValueError(f"Unknown type: {type_}") + row = { + "Variable name": f"`{k}`", + "Description": description, + "Default": f"`{default}`", + "Type": type_, + } + rows.append(row) + + df = pd.DataFrame(rows) + return df.to_markdown(index=False)